diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 31cfb2da1d..7ea71fc103 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -21,6 +21,10 @@ class BankTransaction(StatusUpdater):
self.update_allocations()
self.clear_linked_payment_entries()
self.set_status(update=True)
+
+ def on_cancel(self):
+ self.clear_linked_payment_entries(for_cancel=True)
+ self.set_status(update=True)
def update_allocations(self):
if self.payment_entries:
@@ -41,21 +45,46 @@ class BankTransaction(StatusUpdater):
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
self.reload()
-
- def clear_linked_payment_entries(self):
+
+ def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
- self.clear_simple_entry(payment_entry)
+ self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
- self.clear_sales_invoice(payment_entry)
+ self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
- def clear_simple_entry(self, payment_entry):
- frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date)
+ def clear_simple_entry(self, payment_entry, for_cancel=False):
+ if payment_entry.payment_document == "Payment Entry":
+ if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer":
+ if len(get_reconciled_bank_transactions(payment_entry)) < 2:
+ return
- def clear_sales_invoice(self, payment_entry):
- frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.payment_document,
- parent=payment_entry.payment_entry), "clearance_date", self.date)
+ clearance_date = self.date if not for_cancel else None
+ frappe.db.set_value(
+ payment_entry.payment_document, payment_entry.payment_entry,
+ "clearance_date", clearance_date)
+
+ def clear_sales_invoice(self, payment_entry, for_cancel=False):
+ clearance_date = self.date if not for_cancel else None
+ frappe.db.set_value(
+ "Sales Invoice Payment",
+ dict(
+ parenttype=payment_entry.payment_document,
+ parent=payment_entry.payment_entry
+ ),
+ "clearance_date", clearance_date)
+
+def get_reconciled_bank_transactions(payment_entry):
+ reconciled_bank_transactions = frappe.get_all(
+ 'Bank Transaction Payments',
+ filters = {
+ 'payment_entry': payment_entry.payment_entry
+ },
+ fields = ['parent']
+ )
+
+ return reconciled_bank_transactions
def get_total_allocated_amount(payment_entry):
return frappe.db.sql("""
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js
index bff41d5539..2585ee9c92 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js
@@ -4,10 +4,12 @@
frappe.listview_settings['Bank Transaction'] = {
add_fields: ["unallocated_amount"],
get_indicator: function(doc) {
- if(flt(doc.unallocated_amount)>0) {
- return [__("Unreconciled"), "orange", "unallocated_amount,>,0"];
+ if(doc.docstatus == 2) {
+ return [__("Cancelled"), "red", "docstatus,=,2"];
} else if(flt(doc.unallocated_amount)<=0) {
return [__("Reconciled"), "green", "unallocated_amount,=,0"];
+ } else if(flt(doc.unallocated_amount)>0) {
+ return [__("Unreconciled"), "orange", "unallocated_amount,>,0"];
}
}
};
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index ce149f96e6..439d489119 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -25,7 +25,8 @@ class TestBankTransaction(unittest.TestCase):
def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
- doc.cancel()
+ if doc.docstatus == 1:
+ doc.cancel()
doc.delete()
# Delete directly in DB to avoid validation errors for countries not allowing deletion
@@ -57,6 +58,12 @@ class TestBankTransaction(unittest.TestCase):
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertTrue(clearance_date is not None)
+ bank_transaction.reload()
+ bank_transaction.cancel()
+
+ clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
+ self.assertFalse(clearance_date)
+
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py
index cd8e204f4c..2ba21397ad 100644
--- a/erpnext/accounts/doctype/finance_book/test_finance_book.py
+++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py
@@ -9,19 +9,8 @@ import frappe
import unittest
class TestFinanceBook(unittest.TestCase):
- def create_finance_book(self):
- if not frappe.db.exists("Finance Book", "_Test Finance Book"):
- finance_book = frappe.get_doc({
- "doctype": "Finance Book",
- "finance_book_name": "_Test Finance Book"
- }).insert()
- else:
- finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
-
- return finance_book
-
def test_finance_book(self):
- finance_book = self.create_finance_book()
+ finance_book = create_finance_book()
# create jv entry
jv = make_journal_entry("_Test Bank - _TC",
@@ -41,3 +30,14 @@ class TestFinanceBook(unittest.TestCase):
for gl_entry in gl_entries:
self.assertEqual(gl_entry.finance_book, finance_book.name)
+
+def create_finance_book():
+ if not frappe.db.exists("Finance Book", "_Test Finance Book"):
+ finance_book = frappe.get_doc({
+ "doctype": "Finance Book",
+ "finance_book_name": "_Test Finance Book"
+ }).insert()
+ else:
+ finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
+
+ return finance_book
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 439b1edbce..d96bc271ef 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -533,8 +533,8 @@ frappe.ui.form.on('Payment Entry', {
source_exchange_rate: function(frm) {
if (frm.doc.paid_amount) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
- if(!frm.set_paid_amount_based_on_received_amount &&
- (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) {
+ // target exchange rate should always be same as source if both account currencies is same
+ if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
}
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index d2dffde5cd..abacee985c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -55,14 +55,17 @@ class PaymentEntry(AccountsController):
self.validate_mandatory()
self.validate_reference_documents()
self.set_tax_withholding()
- self.apply_taxes()
self.set_amounts()
+ self.validate_amounts()
+ self.apply_taxes()
+ self.set_amounts_after_tax()
self.clear_unallocated_reference_document_rows()
self.validate_payment_against_negative_invoice()
self.validate_transaction_reference()
self.set_title()
self.set_remarks()
self.validate_duplicate_entry()
+ self.validate_payment_type_with_outstanding()
self.validate_allocated_amount()
self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked()
@@ -118,6 +121,11 @@ class PaymentEntry(AccountsController):
if not self.get(field):
self.set(field, bank_data.account)
+ def validate_payment_type_with_outstanding(self):
+ total_outstanding = sum(d.allocated_amount for d in self.get('references'))
+ if total_outstanding < 0 and self.party_type == 'Customer' and self.payment_type == 'Receive':
+ frappe.throw(_("Cannot receive from customer against negative outstanding"), title=_("Incorrect Payment Type"))
+
def validate_allocated_amount(self):
for d in self.get("references"):
if (flt(d.allocated_amount))> 0:
@@ -236,7 +244,9 @@ class PaymentEntry(AccountsController):
self.company_currency, self.posting_date)
def set_target_exchange_rate(self, ref_doc=None):
- if self.paid_to and not self.target_exchange_rate:
+ if self.paid_from_account_currency == self.paid_to_account_currency:
+ self.target_exchange_rate = self.source_exchange_rate
+ elif self.paid_to and not self.target_exchange_rate:
if ref_doc:
if self.paid_to_account_currency == ref_doc.currency:
self.target_exchange_rate = ref_doc.get("exchange_rate")
@@ -468,13 +478,22 @@ class PaymentEntry(AccountsController):
def set_amounts(self):
self.set_received_amount()
self.set_amounts_in_company_currency()
- self.set_amounts_after_tax()
self.set_total_allocated_amount()
self.set_unallocated_amount()
self.set_difference_amount()
+ def validate_amounts(self):
+ self.validate_received_amount()
+
+ def validate_received_amount(self):
+ if self.paid_from_account_currency == self.paid_to_account_currency:
+ if self.paid_amount != self.received_amount:
+ frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
+
def set_received_amount(self):
self.base_received_amount = self.base_paid_amount
+ if self.paid_from_account_currency == self.paid_to_account_currency:
+ self.received_amount = self.paid_amount
def set_amounts_after_tax(self):
applicable_tax = 0
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 801dadc7f1..dac927b2ce 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -107,7 +107,7 @@ class TestPaymentEntry(unittest.TestCase):
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
- pe.target_exchange_rate = 50
+ pe.source_exchange_rate = 50
pe.insert()
pe.submit()
@@ -154,7 +154,7 @@ class TestPaymentEntry(unittest.TestCase):
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
- pe.target_exchange_rate = 50
+ pe.source_exchange_rate = 50
pe.insert()
pe.submit()
@@ -491,7 +491,7 @@ class TestPaymentEntry(unittest.TestCase):
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
- pe.target_exchange_rate = 55
+ pe.source_exchange_rate = 55
pe.append("deductions", {
"account": "_Test Exchange Gain/Loss - _TC",
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index a6e3bd98e7..289278ea8d 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -50,9 +50,13 @@ class PeriodClosingVoucher(AccountsController):
.format(pce[0][0], self.posting_date))
def make_gl_entries(self):
+ gl_entries = self.get_gl_entries()
+ if gl_entries:
+ from erpnext.accounts.general_ledger import make_gl_entries
+ make_gl_entries(gl_entries)
+
+ def get_gl_entries(self):
gl_entries = []
- net_pl_balance = 0
-
pl_accounts = self.get_pl_balances()
for acc in pl_accounts:
@@ -60,6 +64,7 @@ class PeriodClosingVoucher(AccountsController):
gl_entries.append(self.get_gl_dict({
"account": acc.account,
"cost_center": acc.cost_center,
+ "finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
@@ -67,35 +72,13 @@ class PeriodClosingVoucher(AccountsController):
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
}, item=acc))
- net_pl_balance += flt(acc.bal_in_company_currency)
+ if gl_entries:
+ gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
+ gl_entries += gle_for_net_pl_bal
- if net_pl_balance:
- if self.cost_center_wise_pnl:
- costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts)
- gl_entries += costcenter_wise_gl_entries
- else:
- gl_entry = self.get_pnl_gl_entry(net_pl_balance)
- gl_entries.append(gl_entry)
-
- from erpnext.accounts.general_ledger import make_gl_entries
- make_gl_entries(gl_entries)
-
- def get_pnl_gl_entry(self, net_pl_balance):
- cost_center = frappe.db.get_value("Company", self.company, "cost_center")
- gl_entry = self.get_gl_dict({
- "account": self.closing_account_head,
- "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0,
- "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0,
- "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0,
- "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0,
- "cost_center": cost_center
- })
-
- self.update_default_dimensions(gl_entry)
-
- return gl_entry
-
- def get_costcenter_wise_pnl_gl_entries(self, pl_accounts):
+ return gl_entries
+
+ def get_pnl_gl_entry(self, pl_accounts):
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entries = []
@@ -104,6 +87,7 @@ class PeriodClosingVoucher(AccountsController):
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"cost_center": acc.cost_center or company_cost_center,
+ "finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
@@ -130,7 +114,7 @@ class PeriodClosingVoucher(AccountsController):
def get_pl_balances(self):
"""Get balance for dimension-wise pl accounts"""
- dimension_fields = ['t1.cost_center']
+ dimension_fields = ['t1.cost_center', 't1.finance_book']
self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions:
diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
index 18f9549fee..da0eeaca8d 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
@@ -8,6 +8,7 @@ import frappe
from frappe.utils import flt, today
from erpnext.accounts.utils import get_fiscal_year, now
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestPeriodClosingVoucher(unittest.TestCase):
@@ -120,6 +121,58 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertTrue(pcv_gle, expected_gle)
+ def test_period_closing_with_finance_book_entries(self):
+ frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
+
+ company = create_company()
+ surplus_account = create_account()
+ cost_center = create_cost_center("Test Cost Center 1")
+
+ create_sales_invoice(
+ company=company,
+ income_account="Sales - TPC",
+ expense_account="Cost of Goods Sold - TPC",
+ cost_center=cost_center,
+ rate=400,
+ debit_to="Debtors - TPC"
+ )
+ jv = make_journal_entry(
+ account1="Cash - TPC",
+ account2="Sales - TPC",
+ amount=400,
+ cost_center=cost_center,
+ posting_date=now()
+ )
+ jv.company = company
+ jv.finance_book = create_finance_book().name
+ jv.save()
+ jv.submit()
+
+ pcv = frappe.get_doc({
+ "transaction_date": today(),
+ "posting_date": today(),
+ "fiscal_year": get_fiscal_year(today())[0],
+ "company": company,
+ "closing_account_head": surplus_account,
+ "remarks": "Test",
+ "doctype": "Period Closing Voucher"
+ })
+ pcv.insert()
+ pcv.submit()
+
+ expected_gle = (
+ (surplus_account, 0.0, 400.0, ''),
+ (surplus_account, 0.0, 400.0, jv.finance_book),
+ ('Sales - TPC', 400.0, 0.0, ''),
+ ('Sales - TPC', 400.0, 0.0, jv.finance_book)
+ )
+
+ pcv_gle = frappe.db.sql("""
+ select account, debit, credit, finance_book from `tabGL Entry` where voucher_no=%s
+ """, (pcv.name))
+
+ self.assertTrue(pcv_gle, expected_gle)
+
def make_period_closing_voucher(self):
pcv = frappe.get_doc({
"doctype": "Period Closing Voucher",
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
index b596c0cf25..5b18ebb40d 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -85,9 +85,15 @@ class TestPOSClosingEntry(unittest.TestCase):
pcv_doc.load_from_db()
pcv_doc.cancel()
- si_doc.load_from_db()
+
+ cancelled_invoice = frappe.db.get_value(
+ 'POS Invoice Merge Log', {'pos_closing_entry': pcv_doc.name},
+ 'consolidated_invoice'
+ )
+ docstatus = frappe.db.get_value("Sales Invoice", cancelled_invoice, 'docstatus')
+ self.assertEqual(docstatus, 2)
+
pos_inv1.load_from_db()
- self.assertEqual(si_doc.docstatus, 2)
self.assertEqual(pos_inv1.status, 'Paid')
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 181e9f8ec0..15c292211c 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -16,7 +16,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
onload(doc) {
super.onload();
- this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
+ this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry'];
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields();
@@ -111,16 +111,12 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
}
write_off_outstanding_amount_automatically() {
- if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
+ if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]);
// this will make outstanding amount 0
this.frm.set_value("write_off_amount",
flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount"))
);
- this.frm.toggle_enable("write_off_amount", false);
-
- } else {
- this.frm.toggle_enable("write_off_amount", true);
}
this.calculate_outstanding_amount(false);
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index fcccb39b70..19c6c8f347 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -99,6 +99,7 @@
"loyalty_redemption_account",
"loyalty_redemption_cost_center",
"section_break_49",
+ "coupon_code",
"apply_discount_on",
"base_discount_amount",
"column_break_51",
@@ -1183,7 +1184,8 @@
"label": "Write Off Amount",
"no_copy": 1,
"options": "currency",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only_depends_on": "eval: doc.write_off_outstanding_amount_automatically"
},
{
"fieldname": "base_write_off_amount",
@@ -1549,12 +1551,20 @@
"no_copy": 1,
"options": "Sales Invoice",
"read_only": 1
+ },
+ {
+ "depends_on": "coupon_code",
+ "fieldname": "coupon_code",
+ "fieldtype": "Link",
+ "label": "Coupon Code",
+ "options": "Coupon Code",
+ "print_hide": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2021-08-17 20:13:44.255437",
+ "modified": "2021-08-24 18:19:20.728433",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 8ec4ef224c..034a217a26 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -44,6 +44,9 @@ class POSInvoice(SalesInvoice):
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
+ if self.coupon_code:
+ from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
+ validate_coupon_code(self.coupon_code)
def on_submit(self):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
@@ -58,6 +61,10 @@ class POSInvoice(SalesInvoice):
self.check_phone_payments()
self.set_status(update=True)
+ if self.coupon_code:
+ from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
+ update_coupon_code_count(self.coupon_code,'used')
+
def before_cancel(self):
if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
pos_closing_entry = frappe.get_all(
@@ -84,6 +91,10 @@ class POSInvoice(SalesInvoice):
against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry()
+ if self.coupon_code:
+ from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
+ update_coupon_code_count(self.coupon_code,'cancelled')
+
def check_phone_payments(self):
for pay in self.payments:
if pay.type == "Phone" and pay.amount >= 0:
@@ -127,7 +138,7 @@ class POSInvoice(SalesInvoice):
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_stock_availablility(self):
- if self.is_return:
+ if self.is_return or self.docstatus != 1:
return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 6172796129..d2527fb2e5 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -320,7 +320,8 @@ class TestPOSInvoice(unittest.TestCase):
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
- self.assertRaises(frappe.ValidationError, pos2.insert)
+ pos2.insert()
+ self.assertRaises(frappe.ValidationError, pos2.submit)
def test_delivered_serialized_item_transaction(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -348,7 +349,8 @@ class TestPOSInvoice(unittest.TestCase):
pos2.get("items")[0].serial_no = serial_nos[0]
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
- self.assertRaises(frappe.ValidationError, pos2.insert)
+ pos2.insert()
+ self.assertRaises(frappe.ValidationError, pos2.submit)
def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 556f49d34c..4903c50e17 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -198,12 +198,19 @@ def apply_pricing_rule(args, doc=None):
set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings",
"automatically_set_serial_nos_based_on_fifo")
+ item_code_list = tuple(item.get('item_code') for item in item_list)
+ query_items = frappe.get_all('Item', fields=['item_code','has_serial_no'], filters=[['item_code','in',item_code_list]],as_list=1)
+ serialized_items = dict()
+ for item_code, val in query_items:
+ serialized_items.setdefault(item_code, val)
+
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc)
out.append(data)
- if not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'):
+
+ if serialized_items.get(item.get('item_code')) and not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'):
out[0].update(get_serial_no_for_item(args_copy))
return out
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 8d65101b3b..2dd3d690e9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -154,9 +154,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
return
}
- $.each(doc["items"], function(i, row) {
+ doc.items.forEach((row) => {
if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note)
- })
+ });
}
set_default_print_format() {
@@ -324,16 +324,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
write_off_outstanding_amount_automatically() {
- if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
+ if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]);
// this will make outstanding amount 0
this.frm.set_value("write_off_amount",
flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount"))
);
- this.frm.toggle_enable("write_off_amount", false);
-
- } else {
- this.frm.toggle_enable("write_off_amount", true);
}
this.calculate_outstanding_amount(false);
@@ -450,13 +446,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
currency() {
- super.currency();
+ this._super();
$.each(cur_frm.doc.timesheets, function(i, d) {
let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
});
calculate_total_billing_amount(cur_frm)
}
+
+ currency() {
+ var me = this;
+ super.currency();
+ if (this.frm.doc.timesheets) {
+ this.frm.doc.timesheets.forEach((d) => {
+ let row = frappe.get_doc(d.doctype, d.name)
+ set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
+ });
+ calculate_total_billing_amount(this.frm);
+ }
+ }
};
// for backward compatibility: combine new and previous states
@@ -787,8 +795,6 @@ frappe.ui.form.on('Sales Invoice', {
if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']);
else hide_field(['c_form_applicable', 'c_form_no']);
- frm.toggle_enable("write_off_amount", !!!cint(doc.write_off_outstanding_amount_automatically));
-
frm.refresh_fields();
},
@@ -980,9 +986,9 @@ var calculate_total_billing_amount = function(frm) {
doc.total_billing_amount = 0.0
if (doc.timesheets) {
- $.each(doc.timesheets, function(index, data){
- doc.total_billing_amount += flt(data.billing_amount)
- })
+ doc.timesheets.forEach((d) => {
+ doc.total_billing_amount += flt(d.billing_amount)
+ });
}
refresh_field('total_billing_amount')
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index e317443b91..d8aa32e224 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -247,7 +247,7 @@
"depends_on": "customer",
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1,
@@ -692,10 +692,11 @@
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
- "options": "Barcode",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Scan Barcode"
+ "label": "Scan Barcode",
+ "length": 1,
+ "options": "Barcode"
},
{
"allow_bulk_edit": 1,
@@ -1059,6 +1060,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Apply Additional Discount On",
+ "length": 15,
"options": "\nGrand Total\nNet Total",
"print_hide": 1
},
@@ -1145,7 +1147,7 @@
{
"description": "In Words will be visible once you save the Sales Invoice.",
"fieldname": "base_in_words",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words (Company Currency)",
@@ -1205,7 +1207,7 @@
},
{
"fieldname": "in_words",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words",
@@ -1444,7 +1446,8 @@
"label": "Write Off Amount",
"no_copy": 1,
"options": "currency",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only_depends_on": "eval:doc.write_off_outstanding_amount_automatically"
},
{
"fieldname": "base_write_off_amount",
@@ -1557,6 +1560,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Print Language",
+ "length": 6,
"print_hide": 1,
"read_only": 1
},
@@ -1644,6 +1648,7 @@
"hide_seconds": 1,
"in_standard_filter": 1,
"label": "Status",
+ "length": 30,
"no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
@@ -1703,6 +1708,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Opening Entry",
+ "length": 4,
"oldfieldname": "is_opening",
"oldfieldtype": "Select",
"options": "No\nYes",
@@ -1714,6 +1720,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "C-Form Applicable",
+ "length": 4,
"no_copy": 1,
"options": "No\nYes",
"print_hide": 1
@@ -2014,7 +2021,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-08-17 20:16:12.737743",
+ "modified": "2021-08-25 14:46:05.279588",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 984a652248..c3d83c7d74 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -26,6 +26,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.utils import get_incoming_rate
+from erpnext.accounts.utils import PaymentEntryUnlinkError
class TestSalesInvoice(unittest.TestCase):
def make(self):
@@ -136,7 +137,7 @@ class TestSalesInvoice(unittest.TestCase):
pe.paid_to_account_currency = si.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
- pe.paid_amount = si.grand_total
+ pe.paid_amount = si.outstanding_amount
pe.insert()
pe.submit()
@@ -145,6 +146,42 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.LinkExistsError, si.cancel)
unlink_payment_on_cancel_of_invoice()
+ def test_payment_entry_unlink_against_standalone_credit_note(self):
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+ si1 = create_sales_invoice(rate=1000)
+ si2 = create_sales_invoice(rate=300)
+ si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
+
+
+ pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC")
+ pe.append('references', {
+ 'reference_doctype': 'Sales Invoice',
+ 'reference_name': si2.name,
+ 'total_amount': si2.grand_total,
+ 'outstanding_amount': si2.outstanding_amount,
+ 'allocated_amount': si2.outstanding_amount
+ })
+
+ pe.append('references', {
+ 'reference_doctype': 'Sales Invoice',
+ 'reference_name': si3.name,
+ 'total_amount': si3.grand_total,
+ 'outstanding_amount': si3.outstanding_amount,
+ 'allocated_amount': si3.outstanding_amount
+ })
+
+ pe.reference_no = 'Test001'
+ pe.reference_date = nowdate()
+ pe.save()
+ pe.submit()
+
+ si2.load_from_db()
+ si2.cancel()
+
+ si1.load_from_db()
+ self.assertRaises(PaymentEntryUnlinkError, si1.cancel)
+
+
def test_sales_invoice_calculation_export_currency(self):
si = frappe.copy_doc(test_records[2])
si.currency = "USD"
@@ -2014,7 +2051,7 @@ class TestSalesInvoice(unittest.TestCase):
data = get_ewb_data("Sales Invoice", [si.name])
- self.assertEqual(data['version'], '1.0.1118')
+ self.assertEqual(data['version'], '1.0.0421')
self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR')
self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company')
self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer')
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 7c4ff73d90..8bf7b78f58 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -367,21 +367,25 @@ class Subscription(Document):
)
# Discounts
- if self.additional_discount_percentage:
- invoice.additional_discount_percentage = self.additional_discount_percentage
+ if self.is_trialling():
+ invoice.additional_discount_percentage = 100
+ else:
+ if self.additional_discount_percentage:
+ invoice.additional_discount_percentage = self.additional_discount_percentage
- if self.additional_discount_amount:
- invoice.discount_amount = self.additional_discount_amount
+ if self.additional_discount_amount:
+ invoice.discount_amount = self.additional_discount_amount
- if self.additional_discount_percentage or self.additional_discount_amount:
- discount_on = self.apply_additional_discount
- invoice.apply_discount_on = discount_on if discount_on else 'Grand Total'
+ if self.additional_discount_percentage or self.additional_discount_amount:
+ discount_on = self.apply_additional_discount
+ invoice.apply_discount_on = discount_on if discount_on else 'Grand Total'
# Subscription period
invoice.from_date = self.current_invoice_start
invoice.to_date = self.current_invoice_end
invoice.flags.ignore_mandatory = True
+
invoice.save()
if self.submit_invoice:
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 1536a237de..0cb872c4b8 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -240,14 +240,15 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0
invoice_filters = {
- 'name': ('in', vouchers),
- 'docstatus': 1
+ 'name': ('in', vouchers),
+ 'docstatus': 1,
+ 'apply_tds': 1
}
field = 'sum(net_total)'
- if not cint(tax_details.consider_party_ledger_amount):
- invoice_filters.update({'apply_tds': 1})
+ if cint(tax_details.consider_party_ledger_amount):
+ invoice_filters.pop('apply_tds', None)
field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 1c687e5cb1..0f921db678 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -145,6 +145,36 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
+ def test_tds_calculation_on_net_total(self):
+ frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS")
+ invoices = []
+
+ pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True)
+ pi.append('taxes', {
+ "category": "Total",
+ "charge_type": "Actual",
+ "account_head": '_Test Account VAT - _TC',
+ "cost_center": 'Main - _TC',
+ "tax_amount": 1000,
+ "description": "Test",
+ "add_deduct_tax": "Add"
+
+ })
+ pi.save()
+ pi.submit()
+ invoices.append(pi)
+
+ # Second Invoice will apply TDS checked
+ pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000)
+ pi1.submit()
+ invoices.append(pi1)
+
+ self.assertEqual(pi1.taxes[0].tax_amount, 4000)
+
+ #delete invoices to avoid clashing
+ for d in invoices:
+ d.cancel()
+
def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
@@ -220,7 +250,7 @@ def create_sales_invoice(**args):
def create_records():
# create a new suppliers
- for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
+ for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']:
if frappe.db.exists('Supplier', name):
continue
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 4c7c567b42..3126138408 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher',
- 'cost_center', 'against_voucher_type', 'party_type', 'project']
+ 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book']
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index b54646fd27..cedfc0f58b 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -535,6 +535,8 @@ class ReceivablePayableReport(object):
if getdate(entry_date) > getdate(self.filters.report_date):
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
+ row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5
+
def get_ageing_data(self, entry_date, row):
# [0-30, 30-60, 60-90, 90-120, 120-above]
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index e94b30921f..4bfb022c4e 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -82,6 +82,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"range3": 0.0,
"range4": 0.0,
"range5": 0.0,
+ "total_due": 0.0,
"sales_person": []
}))
@@ -135,3 +136,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]),
"{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]):
self.add_column(label=label, fieldname='range' + str(i+1))
+
+ # Add column for total due amount
+ self.add_column(label="Total Amount Due", fieldname='total_due')
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 5d8d49d6a6..3723c8e0d2 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -78,13 +78,10 @@ def validate_filters(filters, account_details):
def validate_party(filters):
party_type, party = filters.get("party_type"), filters.get("party")
- if party:
- if not party_type:
- frappe.throw(_("To filter based on Party, select Party Type first"))
- else:
- for d in party:
- if not frappe.db.exists(party_type, d):
- frappe.throw(_("Invalid {0}: {1}").format(party_type, d))
+ if party and party_type:
+ for d in party:
+ if not frappe.db.exists(party_type, d):
+ frappe.throw(_("Invalid {0}: {1}").format(party_type, d))
def set_account_currency(filters):
if filters.get("account") or (filters.get('party') and len(filters.party) == 1):
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json
index cd6bac2d77..5fff3fdba7 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.json
+++ b/erpnext/accounts/report/gross_profit/gross_profit.json
@@ -1,16 +1,20 @@
{
- "add_total_row": 1,
+ "add_total_row": 0,
+ "columns": [],
"creation": "2013-02-25 17:03:34",
+ "disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
+ "filters": [],
"idx": 3,
"is_standard": "Yes",
- "modified": "2020-08-13 11:26:39.112352",
+ "modified": "2021-08-19 18:57:07.468202",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",
"owner": "Administrator",
+ "prepared_report": 0,
"ref_doctype": "Sales Invoice",
"report_name": "Gross Profit",
"report_type": "Script Report",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 6d8623c189..c949d9b74e 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -41,12 +41,14 @@ def execute(filters=None):
columns = get_columns(group_wise_columns, filters)
- for src in gross_profit_data.grouped_data:
+ for idx, src in enumerate(gross_profit_data.grouped_data):
row = []
for col in group_wise_columns.get(scrub(filters.group_by)):
row.append(src.get(col))
row.append(filters.currency)
+ if idx == len(gross_profit_data.grouped_data)-1:
+ row[0] = frappe.bold("Total")
data.append(row)
return columns, data
@@ -154,6 +156,15 @@ class GrossProfitGenerator(object):
def get_average_rate_based_on_group_by(self):
# sum buying / selling totals for group
+ self.totals = frappe._dict(
+ qty=0,
+ base_amount=0,
+ buying_amount=0,
+ gross_profit=0,
+ gross_profit_percent=0,
+ base_rate=0,
+ buying_rate=0
+ )
for key in list(self.grouped):
if self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]):
@@ -165,6 +176,7 @@ class GrossProfitGenerator(object):
new_row.base_amount += flt(row.base_amount, self.currency_precision)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)
+ self.add_to_totals(new_row)
else:
for i, row in enumerate(self.grouped[key]):
if row.parent in self.returned_invoices \
@@ -177,15 +189,25 @@ class GrossProfitGenerator(object):
if row.qty or row.base_amount:
row = self.set_average_rate(row)
self.grouped_data.append(row)
+ self.add_to_totals(row)
+ self.set_average_gross_profit(self.totals)
+ self.grouped_data.append(self.totals)
def set_average_rate(self, new_row):
+ self.set_average_gross_profit(new_row)
+ new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0
+ new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0
+ return new_row
+
+ def set_average_gross_profit(self, new_row):
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision)
new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \
if new_row.base_amount else 0
- new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0
- new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0
- return new_row
+ def add_to_totals(self, new_row):
+ for key in self.totals:
+ if new_row.get(key):
+ self.totals[key] += new_row[key]
def get_returned_invoice_items(self):
returned_invoices = frappe.db.sql("""
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index eca1e26966..118f628abe 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -19,6 +19,7 @@ from erpnext.stock import get_warehouse_account_map
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
class FiscalYearError(frappe.ValidationError): pass
+class PaymentEntryUnlinkError(frappe.ValidationError): pass
@frappe.whitelist()
def get_fiscal_year(date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False):
@@ -555,10 +556,16 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no))
for pe in linked_pe:
- pe_doc = frappe.get_doc("Payment Entry", pe)
- pe_doc.set_total_allocated_amount()
- pe_doc.set_unallocated_amount()
- pe_doc.clear_unallocated_reference_document_rows()
+ try:
+ pe_doc = frappe.get_doc("Payment Entry", pe)
+ pe_doc.set_amounts()
+ pe_doc.clear_unallocated_reference_document_rows()
+ pe_doc.validate_payment_type_with_outstanding()
+ except Exception as e:
+ msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
+ msg += '
'
+ msg += _("Please cancel payment entry manually first")
+ frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s,
base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 654e0ebca0..ce6a2a4451 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -160,7 +160,8 @@ class AccountsController(TransactionBase):
self.set_due_date()
self.set_payment_schedule()
self.validate_payment_schedule_amount()
- self.validate_due_date()
+ if not self.get('ignore_default_payment_terms_template'):
+ self.validate_due_date()
self.validate_advance_entries()
def validate_non_invoice_documents_schedule(self):
@@ -1853,6 +1854,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
for d in data:
new_child_flag = False
+
+ if not d.get("item_code"):
+ # ignore empty rows
+ continue
+
if not d.get("docname"):
new_child_flag = True
check_doc_permissions(parent, 'create')
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 80ccc6d75b..5ee1f2f7fb 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -329,7 +329,6 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name
- target_doc.price_list_rate = 0
elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
@@ -360,7 +359,6 @@ def make_return_doc(doctype, source_name, target_doc=None):
else:
target_doc.pos_invoice_item = source_doc.name
- target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index da2765dede..fc2cc97e0a 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime
-from frappe import _, throw
+from frappe import _, bold, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor
@@ -16,7 +16,6 @@ from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
-
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency,
self.grand_total)
@@ -169,39 +168,96 @@ class SellingController(StockController):
def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field):
- bold_net_rate = frappe.bold("net rate")
- msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""")
- .format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate)))
- msg += "
"
- msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""")
- .format(get_link_to_form("Selling Settings", "Selling Settings")))
- frappe.throw(msg, title=_("Invalid Selling Price"))
+ throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}.
+ Selling {3} should be atleast {4}.
Alternatively,
+ you can disable selling price validation in {5} to bypass
+ this validation.""").format(
+ idx,
+ bold(item_name),
+ bold(ref_rate_field),
+ bold("net rate"),
+ bold(rate),
+ get_link_to_form("Selling Settings", "Selling Settings"),
+ ), title=_("Invalid Selling Price"))
- if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
- return
- if hasattr(self, "is_return") and self.is_return:
+ if (
+ self.get("is_return")
+ or not frappe.db.get_single_value("Selling Settings", "validate_selling_price")
+ ):
return
- for it in self.get("items"):
- if not it.item_code:
+ is_internal_customer = self.get('is_internal_customer')
+ valuation_rate_map = {}
+
+ for item in self.items:
+ if not item.item_code:
continue
- last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
- last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1)
- if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
- throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate")
+ last_purchase_rate, is_stock_item = frappe.get_cached_value(
+ "Item", item.item_code, ("last_purchase_rate", "is_stock_item")
+ )
- last_valuation_rate = frappe.db.sql("""
- SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s
- AND warehouse = %s AND valuation_rate > 0
- ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1
- """, (it.item_code, it.warehouse))
- if last_valuation_rate:
- last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1)
- if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \
- and not self.get('is_internal_customer'):
- throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")
+ last_purchase_rate_in_sales_uom = (
+ last_purchase_rate * (item.conversion_factor or 1)
+ )
+ if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
+ throw_message(
+ item.idx,
+ item.item_name,
+ last_purchase_rate_in_sales_uom,
+ "last purchase rate"
+ )
+
+ if is_internal_customer or not is_stock_item:
+ continue
+
+ valuation_rate_map[(item.item_code, item.warehouse)] = None
+
+ if not valuation_rate_map:
+ return
+
+ or_conditions = (
+ f"""(item_code = {frappe.db.escape(valuation_rate[0])}
+ and warehouse = {frappe.db.escape(valuation_rate[1])})"""
+ for valuation_rate in valuation_rate_map
+ )
+
+ valuation_rates = frappe.db.sql(f"""
+ select
+ item_code, warehouse, valuation_rate
+ from
+ `tabBin`
+ where
+ ({" or ".join(or_conditions)})
+ and valuation_rate > 0
+ """, as_dict=True)
+
+ for rate in valuation_rates:
+ valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
+
+ for item in self.items:
+ if not item.item_code:
+ continue
+
+ last_valuation_rate = valuation_rate_map.get(
+ (item.item_code, item.warehouse)
+ )
+
+ if not last_valuation_rate:
+ continue
+
+ last_valuation_rate_in_sales_uom = (
+ last_valuation_rate * (item.conversion_factor or 1)
+ )
+
+ if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
+ throw_message(
+ item.idx,
+ item.item_name,
+ last_valuation_rate_in_sales_uom,
+ "valuation rate"
+ )
def get_item_list(self):
il = []
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index b1f89b08d7..7b24e50b14 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -86,7 +86,8 @@ status_map = {
],
"Bank Transaction": [
["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"],
- ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"]
+ ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"],
+ ["Cancelled", "eval:self.docstatus == 2"]
],
"POS Opening Entry": [
["Draft", None],
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 05edb2530c..7c6d3552f1 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -595,7 +595,8 @@ class calculate_taxes_and_totals(object):
self.doc.precision("outstanding_amount"))
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
- self.update_paid_amount_for_return(total_amount_to_pay)
+ self.set_total_amount_to_default_mop(total_amount_to_pay)
+ self.calculate_paid_amount()
def calculate_paid_amount(self):
@@ -675,7 +676,7 @@ class calculate_taxes_and_totals(object):
def set_item_wise_tax_breakup(self):
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
- def update_paid_amount_for_return(self, total_amount_to_pay):
+ def set_total_amount_to_default_mop(self, total_amount_to_pay):
default_mode_of_payment = frappe.db.get_value('POS Payment Method',
{'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
@@ -685,9 +686,7 @@ class calculate_taxes_and_totals(object):
'mode_of_payment': default_mode_of_payment.mode_of_payment,
'amount': total_amount_to_pay,
'default': 1
- })
-
- self.calculate_paid_amount()
+ })
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index c0ce6badbf..cad17a3bee 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -36,7 +36,8 @@ class Lead(SellingController):
})
def set_full_name(self):
- self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
+ if self.first_name:
+ self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
def validate_email_id(self):
if self.email_id:
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
index 263005ef6c..7aa0b77759 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
@@ -2,8 +2,8 @@
// For license information, please see license.txt
frappe.ui.form.on('LinkedIn Settings', {
- onload: function(frm){
- if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
+ onload: function(frm) {
+ if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) {
frappe.confirm(
__('Session not valid, Do you want to login?'),
function(){
@@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', {
}
);
}
+ frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`]));
},
- refresh: function(frm){
+ refresh: function(frm) {
if (frm.doc.session_status=="Expired"){
let msg = __("Session Not Active. Save doc to login.");
frm.dashboard.set_headline_alert(
@@ -53,7 +54,7 @@ frappe.ui.form.on('LinkedIn Settings', {
);
}
},
- login: function(frm){
+ login: function(frm) {
if (frm.doc.consumer_key && frm.doc.consumer_secret){
frappe.dom.freeze();
frappe.call({
@@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', {
});
}
},
- after_save: function(frm){
+ after_save: function(frm) {
frm.trigger("login");
}
});
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
index 9eacb0011c..f882e36c32 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
@@ -2,6 +2,7 @@
"actions": [],
"creation": "2020-01-30 13:36:39.492931",
"doctype": "DocType",
+ "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -87,7 +88,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-04-16 23:22:51.966397",
+ "modified": "2021-02-18 15:19:21.920725",
"modified_by": "Administrator",
"module": "CRM",
"name": "LinkedIn Settings",
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
index d8c6fb4f90..9b88d78c1f 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -3,11 +3,12 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe, requests, json
+import frappe
+import requests
from frappe import _
-from frappe.utils import get_site_url, get_url_to_form, get_link_to_form
+from frappe.utils import get_url_to_form
from frappe.model.document import Document
-from frappe.utils.file_manager import get_file, get_file_path
+from frappe.utils.file_manager import get_file_path
from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document):
@@ -42,11 +43,7 @@ class LinkedInSettings(Document):
self.db_set("access_token", response["access_token"])
def get_member_profile(self):
- headers = {
- "Authorization": "Bearer {}".format(self.access_token)
- }
- url = "https://api.linkedin.com/v2/me"
- response = requests.get(url=url, headers=headers)
+ response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers())
response = frappe.parse_json(response.content.decode())
frappe.db.set_value(self.doctype, self.name, {
@@ -55,16 +52,16 @@ class LinkedInSettings(Document):
"session_status": "Active"
})
frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
+ frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
- def post(self, text, media=None):
+ def post(self, text, title, media=None):
if not media:
- return self.post_text(text)
+ return self.post_text(text, title)
else:
media_id = self.upload_image(media)
if media_id:
- return self.post_text(text, media_id=media_id)
+ return self.post_text(text, title, media_id=media_id)
else:
frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
@@ -82,9 +79,7 @@ class LinkedInSettings(Document):
}]
}
}
- headers = {
- "Authorization": "Bearer {}".format(self.access_token)
- }
+ headers = self.get_headers()
response = self.http_post(url=register_url, body=body, headers=headers)
if response.status_code == 200:
@@ -100,24 +95,33 @@ class LinkedInSettings(Document):
return None
- def post_text(self, text, media_id=None):
+ def post_text(self, text, title, media_id=None):
url = "https://api.linkedin.com/v2/shares"
- headers = {
- "X-Restli-Protocol-Version": "2.0.0",
- "Authorization": "Bearer {}".format(self.access_token),
- "Content-Type": "application/json; charset=UTF-8"
- }
+ headers = self.get_headers()
+ headers["X-Restli-Protocol-Version"] = "2.0.0"
+ headers["Content-Type"] = "application/json; charset=UTF-8"
+
body = {
"distribution": {
"linkedInDistributionTarget": {}
},
"owner":"urn:li:organization:{0}".format(self.company_id),
- "subject": "Test Share Subject",
+ "subject": title,
"text": {
"text": text
}
}
+ reference_url = self.get_reference_url(text)
+ if reference_url:
+ body["content"] = {
+ "contentEntities": [
+ {
+ "entityLocation": reference_url
+ }
+ ]
+ }
+
if media_id:
body["content"]= {
"contentEntities": [{
@@ -141,20 +145,60 @@ class LinkedInSettings(Document):
raise
except Exception as e:
- content = json.loads(response.content)
-
- if response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(content["message"], title="LinkedIn Error - Unauthorized")
- elif response.status_code == 403:
- frappe.msgprint(_("You Didn't have permission to access this API"))
- frappe.throw(content["message"], title="LinkedIn Error - Access Denied")
- else:
- frappe.throw(response.reason, title=response.status_code)
-
+ self.api_error(response)
+
return response
+ def get_headers(self):
+ return {
+ "Authorization": "Bearer {}".format(self.access_token)
+ }
+
+ def get_reference_url(self, text):
+ import re
+ regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
+ urls = re.findall(regex_url, text)
+ if urls:
+ return urls[0]
+
+ def delete_post(self, post_id):
+ try:
+ response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers())
+ if response.status_code !=200:
+ raise
+ except Exception:
+ self.api_error(response)
+
+ def get_post(self, post_id):
+ url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id)
+
+ try:
+ response = requests.get(url=url, headers=self.get_headers())
+ if response.status_code !=200:
+ raise
+
+ except Exception:
+ self.api_error(response)
+
+ response = frappe.parse_json(response.content.decode())
+ if len(response.elements):
+ return response.elements[0]
+
+ return None
+
+ def api_error(self, response):
+ content = frappe.parse_json(response.content.decode())
+
+ if response.status_code == 401:
+ self.db_set("session_status", "Expired")
+ frappe.db.commit()
+ frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized"))
+ elif response.status_code == 403:
+ frappe.msgprint(_("You didn't have permission to access this API"))
+ frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied"))
+ else:
+ frappe.throw(response.reason, title=response.status_code)
+
@frappe.whitelist(allow_guest=True)
def callback(code=None, error=None, error_description=None):
if not error:
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 632012b31d..cb95881cb4 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -95,9 +95,17 @@ frappe.ui.form.on("Opportunity", {
}, __('Create'));
}
- frm.add_custom_button(__('Quotation'),
- cur_frm.cscript.create_quotation, __('Create'));
+ if (frm.doc.opportunity_from != "Customer") {
+ frm.add_custom_button(__('Customer'),
+ function() {
+ frm.trigger("make_customer")
+ }, __('Create'));
+ }
+ frm.add_custom_button(__('Quotation'),
+ function() {
+ frm.trigger("create_quotation")
+ }, __('Create'));
}
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {
@@ -195,6 +203,13 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
frm: cur_frm
})
}
+
+ make_customer() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.crm.doctype.opportunity.opportunity.make_customer",
+ frm: cur_frm
+ })
+ }
};
extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm}));
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 8ce482a3f9..a74a94afd6 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -287,6 +287,24 @@ def make_request_for_quotation(source_name, target_doc=None):
return doclist
+@frappe.whitelist()
+def make_customer(source_name, target_doc=None):
+ def set_missing_values(source, target):
+ if source.opportunity_from == "Lead":
+ target.lead_name = source.party_name
+
+ doclist = get_mapped_doc("Opportunity", source_name, {
+ "Opportunity": {
+ "doctype": "Customer",
+ "field_map": {
+ "currency": "default_currency",
+ "customer_name": "customer_name"
+ }
+ }
+ }, target_doc, set_missing_values)
+
+ return doclist
+
@frappe.whitelist()
def make_supplier_quotation(source_name, target_doc=None):
doclist = get_mapped_doc("Opportunity", source_name, {
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js
index 6fb0f975f4..a8f5deea53 100644
--- a/erpnext/crm/doctype/social_media_post/social_media_post.js
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.js
@@ -1,67 +1,139 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Social Media Post', {
- validate: function(frm){
- if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){
- frappe.throw(__("Select atleast one Social Media from Share on."))
- }
- if (frm.doc.scheduled_time) {
- let scheduled_time = new Date(frm.doc.scheduled_time);
- let date_time = new Date();
- if (scheduled_time.getTime() < date_time.getTime()){
- frappe.throw(__("Invalid Scheduled Time"));
- }
- }
- if (frm.doc.text?.length > 280){
- frappe.throw(__("Length Must be less than 280."))
- }
- },
- refresh: function(frm){
- if (frm.doc.docstatus === 1){
- if (frm.doc.post_status != "Posted"){
- add_post_btn(frm);
- }
- else if (frm.doc.post_status == "Posted"){
- frm.set_df_property('sheduled_time', 'read_only', 1);
- }
+ validate: function(frm) {
+ if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) {
+ frappe.throw(__("Select atleast one Social Media Platform to Share on."));
+ }
+ if (frm.doc.scheduled_time) {
+ let scheduled_time = new Date(frm.doc.scheduled_time);
+ let date_time = new Date();
+ if (scheduled_time.getTime() < date_time.getTime()) {
+ frappe.throw(__("Scheduled Time must be a future time."));
+ }
+ }
+ frm.trigger('validate_tweet_length');
+ },
- let html='';
- if (frm.doc.twitter){
- let color = frm.doc.twitter_post_id ? "green" : "red";
- let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
- html += `
{{ message }}
+{{ message }}
+You don't have no upcoming holidays this {{ frequency }}.
+ {% endif %} +{% endif %} diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html index caa15f386b..04cf922664 100644 --- a/erpnext/templates/includes/rfq/rfq_items.html +++ b/erpnext/templates/includes/rfq/rfq_items.html @@ -1,4 +1,4 @@ -{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %} +{% from "templates/includes/rfq/rfq_macros.html" import item_name_and_description %} {% for d in doc.items %}