Merge branch 'develop' into FIX-ISS-22-23-06369
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 3f985b6..c0eed18 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -31,6 +31,7 @@
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
+ "book_tax_discount_loss",
"print_settings",
"show_inclusive_tax_in_print",
"column_break_12",
@@ -360,6 +361,13 @@
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts"
+ },
+ {
+ "default": "0",
+ "description": "Split Early Payment Discount Loss into Income and Tax Loss",
+ "fieldname": "book_tax_discount_loss",
+ "fieldtype": "Check",
+ "label": "Book Tax Loss on Early Payment Discount"
}
],
"icon": "icon-cog",
@@ -367,7 +375,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-01-02 12:07:42.434214",
+ "modified": "2023-03-28 09:50:20.375233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 80878ac..0817187 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -81,7 +81,7 @@
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
- loan_disbursements = (
+ query = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
@@ -90,17 +90,22 @@
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
+ loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
- .where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
- ).run(as_dict=1)
+ )
+
+ if not self.include_reconciled_entries:
+ query = query.where(loan_disbursement.clearance_date.isnull())
+
+ loan_disbursements = query.run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
@@ -113,16 +118,19 @@
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
+ loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
- .where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
)
+ if not self.include_reconciled_entries:
+ query = query.where(loan_repayment.clearance_date.isnull())
+
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index cb7da17..d6e1be4 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -325,14 +325,14 @@
if template_type == "Blank Template":
for root_type in get_root_types():
- writer.writerow(["", "", "", 1, "", root_type])
+ writer.writerow(["", "", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts():
- writer.writerow(["", "", "", 1, account, "Asset"])
+ writer.writerow(["", "", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types():
writer.writerow(
- ["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
+ ["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
)
else:
writer = get_sample_template(writer)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 2a8e2527..f8969b8 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -244,8 +244,6 @@
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
party_account_currency, "references");
- frm.set_currency_labels(["amount"], company_currency, "deductions");
-
cur_frm.set_df_property("source_exchange_rate", "description",
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index cd5b6d5..c34bddd 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -416,7 +416,7 @@
for ref in self.get("references"):
if ref.payment_term and ref.reference_name:
- key = (ref.payment_term, ref.reference_name)
+ key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += ref.allocated_amount
@@ -424,20 +424,37 @@
payment_schedule = frappe.get_all(
"Payment Schedule",
filters={"parent": ref.reference_name},
- fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
+ fields=[
+ "paid_amount",
+ "payment_amount",
+ "payment_term",
+ "discount",
+ "outstanding",
+ "discount_type",
+ ],
)
for term in payment_schedule:
- invoice_key = (term.payment_term, ref.reference_name)
+ invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
invoice_paid_amount_map.setdefault(invoice_key, {})
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
- invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
- term.discount / 100
- )
+ if not (term.discount_type and term.discount):
+ continue
+
+ if term.discount_type == "Percentage":
+ invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
+ term.discount / 100
+ )
+ else:
+ invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
if not invoice_paid_amount_map.get(key):
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
+ allocated_amount = self.get_allocated_amount_in_transaction_currency(
+ allocated_amount, key[2], key[1]
+ )
+
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
@@ -472,6 +489,33 @@
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
)
+ def get_allocated_amount_in_transaction_currency(
+ self, allocated_amount, reference_doctype, reference_docname
+ ):
+ """
+ Payment Entry could be in base currency while reference's payment schedule
+ is always in transaction currency.
+ E.g.
+ * SI with base=INR and currency=USD
+ * SI with payment schedule in USD
+ * PE in INR (accounting done in base currency)
+ """
+ ref_currency, ref_exchange_rate = frappe.db.get_value(
+ reference_doctype, reference_docname, ["currency", "conversion_rate"]
+ )
+ is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
+ # PE in different currency
+ reference_is_multi_currency = self.paid_from_account_currency != ref_currency
+
+ if not (is_single_currency and reference_is_multi_currency):
+ return allocated_amount
+
+ allocated_amount = flt(
+ allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
+ )
+
+ return allocated_amount
+
def set_status(self):
if self.docstatus == 2:
self.status = "Cancelled"
@@ -1642,7 +1686,14 @@
@frappe.whitelist()
def get_payment_entry(
- dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
+ dt,
+ dn,
+ party_amount=None,
+ bank_account=None,
+ bank_amount=None,
+ party_type=None,
+ payment_type=None,
+ reference_date=None,
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
@@ -1669,8 +1720,9 @@
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
)
- paid_amount, received_amount, discount_amount = apply_early_payment_discount(
- paid_amount, received_amount, doc
+ reference_date = getdate(reference_date)
+ paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
+ paid_amount, received_amount, doc, party_account_currency, reference_date
)
pe = frappe.new_doc("Payment Entry")
@@ -1678,6 +1730,7 @@
pe.company = doc.company
pe.cost_center = doc.get("cost_center")
pe.posting_date = nowdate()
+ pe.reference_date = reference_date
pe.mode_of_payment = doc.get("mode_of_payment")
pe.party_type = party_type
pe.party = doc.get(scrub(party_type))
@@ -1718,7 +1771,7 @@
):
for reference in get_reference_as_per_payment_terms(
- doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+ doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
pe.append("references", reference)
else:
@@ -1769,16 +1822,17 @@
if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
+
if discount_amount:
- pe.set_gain_or_loss(
- account_details={
- "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
- "cost_center": pe.cost_center
- or frappe.get_cached_value("Company", pe.company, "cost_center"),
- "amount": discount_amount * (-1 if payment_type == "Pay" else 1),
- }
+ base_total_discount_loss = 0
+ if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
+ base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
+
+ set_pending_discount_loss(
+ pe, doc, discount_amount, base_total_discount_loss, party_account_currency
)
- pe.set_difference_amount()
+
+ pe.set_difference_amount()
return pe
@@ -1889,20 +1943,28 @@
return paid_amount, received_amount
-def apply_early_payment_discount(paid_amount, received_amount, doc):
+def apply_early_payment_discount(
+ paid_amount, received_amount, doc, party_account_currency, reference_date
+):
total_discount = 0
+ valid_discounts = []
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
+ is_multi_currency = party_account_currency != doc.company_currency
if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
- if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
+ if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
+
if term.discount_type == "Percentage":
- discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
+ grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
+ discount_amount = flt(grand_total) * (term.discount / 100)
else:
discount_amount = term.discount
- discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
+ # if accounting is done in the same currency, paid_amount = received_amount
+ conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
+ discount_amount_in_foreign_currency = discount_amount * conversion_rate
if doc.doctype == "Sales Invoice":
paid_amount -= discount_amount
@@ -1911,23 +1973,151 @@
received_amount -= discount_amount
paid_amount -= discount_amount_in_foreign_currency
+ valid_discounts.append({"type": term.discount_type, "discount": term.discount})
total_discount += discount_amount
if total_discount:
- money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
+ currency = doc.get("currency") if is_multi_currency else doc.company_currency
+ money = frappe.utils.fmt_money(total_discount, currency=currency)
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
- return paid_amount, received_amount, total_discount
+ return paid_amount, received_amount, total_discount, valid_discounts
+
+
+def set_pending_discount_loss(
+ pe, doc, discount_amount, base_total_discount_loss, party_account_currency
+):
+ # If multi-currency, get base discount amount to adjust with base currency deductions/losses
+ if party_account_currency != doc.company_currency:
+ discount_amount = discount_amount * doc.get("conversion_rate", 1)
+
+ # Avoid considering miniscule losses
+ discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
+
+ # Set base discount amount (discount loss/pending rounding loss) in deductions
+ if discount_amount > 0.0:
+ positive_negative = -1 if pe.payment_type == "Pay" else 1
+
+ # If tax loss booking is enabled, pending loss will be rounding loss.
+ # Otherwise it will be the total discount loss.
+ book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
+ account_type = "round_off_account" if book_tax_loss else "default_discount_account"
+
+ pe.set_gain_or_loss(
+ account_details={
+ "account": frappe.get_cached_value("Company", pe.company, account_type),
+ "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": discount_amount * positive_negative,
+ }
+ )
+
+
+def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
+ """Split early payment discount into Income Loss & Tax Loss."""
+ total_discount_percent = get_total_discount_percent(doc, valid_discounts)
+
+ if not total_discount_percent:
+ return 0.0
+
+ base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
+ base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
+
+ # Round off total loss rather than individual losses to reduce rounding error
+ return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
+
+
+def get_total_discount_percent(doc, valid_discounts) -> float:
+ """Get total percentage and amount discount applied as a percentage."""
+ total_discount_percent = (
+ sum(
+ discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
+ )
+ or 0.0
+ )
+
+ # Operate in percentages only as it makes the income & tax split easier
+ total_discount_amount = (
+ sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
+ or 0.0
+ )
+
+ if total_discount_amount:
+ discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
+ total_discount_percent += discount_percentage
+ return total_discount_percent
+
+ return total_discount_percent
+
+
+def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
+ """Add loss on income discount in base currency."""
+ precision = doc.precision("total")
+ base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
+
+ pe.append(
+ "deductions",
+ {
+ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
+ "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": flt(base_loss_on_income, precision),
+ },
+ )
+
+ return base_loss_on_income # Return loss without rounding
+
+
+def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
+ """Add loss on tax discount in base currency."""
+ tax_discount_loss = {}
+ base_total_tax_loss = 0
+ precision = doc.precision("tax_amount_after_discount_amount", "taxes")
+
+ # The same account head could be used more than once
+ for tax in doc.get("taxes", []):
+ base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
+ total_discount_percentage / 100
+ )
+
+ account = tax.get("account_head")
+ if not tax_discount_loss.get(account):
+ tax_discount_loss[account] = base_tax_loss
+ else:
+ tax_discount_loss[account] += base_tax_loss
+
+ for account, loss in tax_discount_loss.items():
+ base_total_tax_loss += loss
+ if loss == 0.0:
+ continue
+
+ pe.append(
+ "deductions",
+ {
+ "account": account,
+ "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": flt(loss, precision),
+ },
+ )
+
+ return base_total_tax_loss # Return loss without rounding
def get_reference_as_per_payment_terms(
- payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+ payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
references = []
+ is_multi_currency_acc = (doc.currency != doc.company_currency) and (
+ party_account_currency != doc.company_currency
+ )
+
for payment_term in payment_schedule:
payment_term_outstanding = flt(
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
)
+ if not is_multi_currency_acc:
+ # If accounting is done in company currency for multi-currency transaction
+ payment_term_outstanding = flt(
+ payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
+ )
if payment_term_outstanding:
references.append(
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 123b5df..67049c4 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -5,7 +5,7 @@
import frappe
from frappe import qb
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@@ -256,10 +256,25 @@
},
)
si.save()
-
si.submit()
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+ pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
+ self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
+ self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
+ self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
+ self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
+ self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+
+ self.assertEqual(pe.references[0].allocated_amount, 236.0)
+ self.assertEqual(pe.paid_amount, 212.4)
+ self.assertEqual(pe.deductions[0].amount, 23.6)
+
pe.submit()
si.load_from_db()
@@ -269,6 +284,190 @@
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
+ def test_payment_entry_against_payment_terms_with_discount_amount(self):
+ si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
+
+ si.payment_terms_template = "Test Discount Amount Template"
+ create_payment_terms_template_with_discount(
+ name="30 Credit Days with Rs.50 Discount",
+ discount_type="Amount",
+ discount=50,
+ template_name="Test Discount Amount Template",
+ )
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
+ si.save()
+ si.submit()
+
+ # Set reference date past discount cut off date
+ pe_1 = get_payment_entry(
+ "Sales Invoice",
+ si.name,
+ bank_account="_Test Cash - _TC",
+ reference_date=frappe.utils.add_days(si.posting_date, 2),
+ )
+ self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
+
+ # Test if tax loss is booked on enabling configuration
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
+ pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+ self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
+ self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
+ self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
+
+ frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+ self.assertEqual(pe.references[0].allocated_amount, 236.0)
+ self.assertEqual(pe.paid_amount, 186)
+ self.assertEqual(pe.deductions[0].amount, 50.0)
+
+ pe.submit()
+ si.load_from_db()
+
+ self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
+ self.assertEqual(si.payment_schedule[0].paid_amount, 186)
+ self.assertEqual(si.payment_schedule[0].outstanding, 0)
+ self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
+
+ @change_settings(
+ "Accounts Settings",
+ {
+ "allow_multi_currency_invoices_against_single_party_account": 1,
+ "book_tax_discount_loss": 1,
+ },
+ )
+ def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
+ self,
+ ):
+ """
+ 1. Multi-currency SI with single currency accounting (company currency)
+ 2. PE with early payment discount
+ 3. Test if Paid Amount is calculated in company currency
+ 4. Test if deductions are calculated in company currency
+
+ SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
+ """
+ si = create_sales_invoice(
+ customer="_Test Customer",
+ currency="USD",
+ conversion_rate=50,
+ do_not_save=1,
+ )
+ create_payment_terms_template_with_discount()
+ si.payment_terms_template = "Test Discount Template"
+
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+ si.save()
+ si.submit()
+
+ pe = get_payment_entry(
+ "Sales Invoice",
+ si.name,
+ bank_account="_Test Bank - _TC",
+ )
+ pe.reference_no = si.name
+ pe.reference_date = nowdate()
+
+ # Early payment discount loss on income
+ self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
+ self.assertEqual(pe.received_amount, 4500.0)
+ self.assertEqual(pe.deductions[0].amount, 500.0)
+ self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
+ self.assertEqual(pe.difference_amount, 0.0)
+
+ pe.insert()
+ pe.submit()
+
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["Debtors - _TC", 0, 5000, si.name],
+ ["_Test Bank - _TC", 4500, 0, None],
+ ["Write Off - _TC", 500.0, 0, None],
+ ]
+ )
+
+ self.validate_gl_entries(pe.name, expected_gle)
+
+ outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
+ self.assertEqual(outstanding_amount, 0)
+
+ def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
+ """
+ 1. Multi-currency SI with multi-currency accounting
+ 2. PE with early payment discount and also exchange loss
+ 3. Test if Paid Amount is calculated in transaction currency
+ 4. Test if deductions are calculated in base/company currency
+ 5. Test if exchange loss is reflected in difference
+ """
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ do_not_save=1,
+ )
+ create_payment_terms_template_with_discount()
+ si.payment_terms_template = "Test Discount Template"
+
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
+ si.save()
+ si.submit()
+
+ pe = get_payment_entry(
+ "Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
+ )
+ pe.reference_no = si.name
+ pe.reference_date = nowdate()
+
+ # Early payment discount loss on income
+ self.assertEqual(pe.paid_amount, 90.0)
+ self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
+ self.assertEqual(pe.deductions[0].amount, 500.0)
+ self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
+
+ # Exchange loss
+ self.assertEqual(pe.difference_amount, 300.0)
+
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 300.0,
+ },
+ )
+
+ pe.insert()
+ pe.submit()
+
+ self.assertEqual(pe.difference_amount, 0.0)
+
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 5000, si.name],
+ ["_Test Bank - _TC", 4200, 0, None],
+ ["Write Off - _TC", 500.0, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
+ ]
+ )
+
+ self.validate_gl_entries(pe.name, expected_gle)
+
+ outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
+ self.assertEqual(outstanding_amount, 0)
+
def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -839,24 +1038,27 @@
).insert()
-def create_payment_terms_template_with_discount():
+def create_payment_terms_template_with_discount(
+ name=None, discount_type=None, discount=None, template_name=None
+):
+ create_payment_term(name or "30 Credit Days with 10% Discount")
+ template_name = template_name or "Test Discount Template"
- create_payment_term("30 Credit Days with 10% Discount")
-
- if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
- payment_term_template = frappe.get_doc(
+ if not frappe.db.exists("Payment Terms Template", template_name):
+ frappe.get_doc(
{
"doctype": "Payment Terms Template",
- "template_name": "Test Discount Template",
+ "template_name": template_name,
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
- "payment_term": "30 Credit Days with 10% Discount",
+ "payment_term": name or "30 Credit Days with 10% Discount",
"invoice_portion": 100,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2,
- "discount": 10,
+ "discount_type": discount_type or "Percentage",
+ "discount": discount or 10,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 1,
}
diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
index 61a1462..1c31829 100644
--- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
+++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
@@ -3,6 +3,7 @@
"creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"account",
"cost_center",
@@ -17,9 +18,7 @@
"in_list_view": 1,
"label": "Account",
"options": "Account",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "cost_center",
@@ -28,37 +27,30 @@
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1,
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Amount",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "label": "Amount (Company Currency)",
+ "options": "Company:company:default_currency",
+ "reqd": 1
},
{
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
- "label": "Description",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-12 20:38:08.110674",
+ "modified": "2023-03-06 07:11:57.739619",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
@@ -66,5 +58,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index d986f32..caffac5 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -272,4 +272,32 @@
}
};
+frappe.ui.form.on('Payment Reconciliation Allocation', {
+ allocated_amount: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ // filter invoice
+ let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
+ // filter payment
+ let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
+
+ frm.call({
+ doc: frm.doc,
+ method: 'calculate_difference_on_allocation_change',
+ args: {
+ payment_entry: payment,
+ invoice: invoice,
+ allocated_amount: row.allocated_amount
+ },
+ callback: (r) => {
+ if (r.message) {
+ row.difference_amount = r.message;
+ frm.refresh();
+ }
+ }
+ });
+ }
+});
+
+
+
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index c9e3998..d8082d0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -234,6 +234,15 @@
return difference_amount
@frappe.whitelist()
+ def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
+ invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
+ invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
+ new_difference_amount = self.get_difference_amount(
+ payment_entry[0], invoice[0], allocated_amount
+ )
+ return new_difference_amount
+
+ @frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index e2b4a1a..5c9168b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -82,7 +82,11 @@
if(doc.docstatus == 1 && doc.outstanding_amount != 0
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
- this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
+ this.frm.add_custom_button(
+ __('Payment'),
+ () => this.make_payment_entry(),
+ __('Create')
+ );
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index b79af71..a617447 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -117,7 +117,7 @@
self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
- self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
+ self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.create_remarks()
self.set_status()
self.validate_purchase_receipt_if_update_stock()
@@ -232,7 +232,7 @@
)
if (
- cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
+ cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
and not self.is_return
and not self.is_internal_supplier
):
@@ -581,6 +581,7 @@
self.make_supplier_gl_entry(gl_entries)
self.make_item_gl_entries(gl_entries)
+ self.make_precision_loss_gl_entry(gl_entries)
if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries)
@@ -975,6 +976,28 @@
item.item_tax_amount, item.precision("item_tax_amount")
)
+ def make_precision_loss_gl_entry(self, gl_entries):
+ round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
+ self.company, "Purchase Invoice", self.name
+ )
+
+ precision_loss = self.get("base_net_total") - flt(
+ self.get("net_total") * self.conversion_rate, self.precision("net_total")
+ )
+
+ if precision_loss:
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": round_off_account,
+ "against": self.supplier,
+ "credit": precision_loss,
+ "cost_center": self.cost_center or round_off_cost_center,
+ "remarks": _("Net total calculation precision loss"),
+ }
+ )
+ )
+
def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 47e3f9b..56e412b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -93,9 +93,12 @@
if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) {
- cur_frm.add_custom_button(__('Payment'),
- this.make_payment_entry, __('Create'));
- cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
+ this.frm.add_custom_button(
+ __('Payment'),
+ () => this.make_payment_entry(),
+ __('Create')
+ );
+ this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if(doc.docstatus==1 && !doc.is_return) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 5cda276..db61995 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -145,7 +145,7 @@
self.set_against_income_account()
self.validate_time_sheets_are_submitted()
- self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
+ self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if not self.is_return:
self.validate_serial_numbers()
else:
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
index 449ebdc..306af72 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import getdate, nowdate
@@ -91,4 +92,65 @@
as_list=1,
)
- return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate()))
+ # Loan Disbursement
+ loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+
+ query = (
+ frappe.qb.from_(loan_disbursement)
+ .select(
+ ConstantColumn("Loan Disbursement").as_("payment_document_type"),
+ loan_disbursement.name.as_("payment_entry"),
+ loan_disbursement.disbursement_date.as_("posting_date"),
+ loan_disbursement.reference_number.as_("cheque_no"),
+ loan_disbursement.clearance_date.as_("clearance_date"),
+ loan_disbursement.applicant.as_("against"),
+ -loan_disbursement.disbursed_amount.as_("amount"),
+ )
+ .where(loan_disbursement.docstatus == 1)
+ .where(loan_disbursement.disbursement_date >= filters["from_date"])
+ .where(loan_disbursement.disbursement_date <= filters["to_date"])
+ .where(loan_disbursement.disbursement_account == filters["account"])
+ .orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc)
+ .orderby(loan_disbursement.name, order=frappe.qb.desc)
+ )
+
+ if filters.get("from_date"):
+ query = query.where(loan_disbursement.disbursement_date >= filters["from_date"])
+ if filters.get("to_date"):
+ query = query.where(loan_disbursement.disbursement_date <= filters["to_date"])
+
+ loan_disbursements = query.run(as_list=1)
+
+ # Loan Repayment
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+
+ query = (
+ frappe.qb.from_(loan_repayment)
+ .select(
+ ConstantColumn("Loan Repayment").as_("payment_document_type"),
+ loan_repayment.name.as_("payment_entry"),
+ loan_repayment.posting_date.as_("posting_date"),
+ loan_repayment.reference_number.as_("cheque_no"),
+ loan_repayment.clearance_date.as_("clearance_date"),
+ loan_repayment.applicant.as_("against"),
+ loan_repayment.amount_paid.as_("amount"),
+ )
+ .where(loan_repayment.docstatus == 1)
+ .where(loan_repayment.posting_date >= filters["from_date"])
+ .where(loan_repayment.posting_date <= filters["to_date"])
+ .where(loan_repayment.payment_account == filters["account"])
+ .orderby(loan_repayment.posting_date, order=frappe.qb.desc)
+ .orderby(loan_repayment.name, order=frappe.qb.desc)
+ )
+
+ if filters.get("from_date"):
+ query = query.where(loan_repayment.posting_date >= filters["from_date"])
+ if filters.get("to_date"):
+ query = query.where(loan_repayment.posting_date <= filters["to_date"])
+
+ loan_repayments = query.run(as_list=1)
+
+ return sorted(
+ journal_entries + payment_entries + loan_disbursements + loan_repayments,
+ key=lambda k: k[2] or getdate(nowdate()),
+ )
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 47089f7..c6c9f1f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -236,7 +236,11 @@
this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
- cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
+ this.frm.add_custom_button(
+ __('Payment'),
+ () => this.make_payment_entry(),
+ __('Create')
+ );
}
if(flt(doc.per_billed) < 100) {
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 3705fcf..390af0d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -515,6 +515,8 @@
parent_dict.update({"customer": parent_dict.get("party_name")})
self.pricing_rules = []
+ basic_item_details_map = {}
+
for item in self.get("items"):
if item.get("item_code"):
args = parent_dict.copy()
@@ -533,7 +535,17 @@
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
- ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
+ basic_details = basic_item_details_map.get(item.item_code)
+ ret, basic_item_details = get_item_details(
+ args,
+ self,
+ for_validate=True,
+ overwrite_warehouse=False,
+ return_basic_details=True,
+ basic_details=basic_details,
+ )
+
+ basic_item_details_map.setdefault(item.item_code, basic_item_details)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
@@ -1232,7 +1244,7 @@
)
)
- def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
+ def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
from erpnext.controllers.status_updater import get_allowance_for
item_allowance = {}
@@ -1245,17 +1257,20 @@
total_overbilled_amt = 0.0
+ reference_names = [d.get(item_ref_dn) for d in self.get("items") if d.get(item_ref_dn)]
+ reference_details = self.get_billing_reference_details(
+ reference_names, ref_dt + " Item", based_on
+ )
+
for item in self.get("items"):
if not item.get(item_ref_dn):
continue
- ref_amt = flt(
- frappe.db.get_value(ref_dt + " Item", item.get(item_ref_dn), based_on),
- self.precision(based_on, item),
- )
+ ref_amt = flt(reference_details.get(item.get(item_ref_dn)), self.precision(based_on, item))
+
if not ref_amt:
frappe.msgprint(
- _("System will not check overbilling since amount for Item {0} in {1} is zero").format(
+ _("System will not check over billing since amount for Item {0} in {1} is zero").format(
item.item_code, ref_dt
),
title=_("Warning"),
@@ -1302,6 +1317,16 @@
alert=True,
)
+ def get_billing_reference_details(self, reference_names, reference_doctype, based_on):
+ return frappe._dict(
+ frappe.get_all(
+ reference_doctype,
+ filters={"name": ("in", reference_names)},
+ fields=["name", based_on],
+ as_list=1,
+ )
+ )
+
def get_billed_amount_for_item(self, item, item_ref_dn, based_on):
"""
Returns Sum of Amount of
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 619a415..a085af8 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -943,7 +943,8 @@
2) If no value, get last valuation rate from SLE
3) If no value, get valuation rate from Item
"""
- from frappe.query_builder.functions import Sum
+ from frappe.query_builder.functions import Count, IfNull, Sum
+ from pypika import Case
item_code, company = data.get("item_code"), data.get("company")
valuation_rate = 0.0
@@ -954,7 +955,14 @@
frappe.qb.from_(bin_table)
.join(wh_table)
.on(bin_table.warehouse == wh_table.name)
- .select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
+ .select(
+ Case()
+ .when(
+ Count(bin_table.name) > 0, IfNull(Sum(bin_table.stock_value) / Sum(bin_table.actual_qty), 0.0)
+ )
+ .else_(None)
+ .as_("valuation_rate")
+ )
.where((bin_table.item_code == item_code) & (wh_table.company == company))
).run(as_dict=True)[0]
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 2624daa..fdaa4a2 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -344,6 +344,7 @@
{
"fieldname": "prod_plan_references",
"fieldtype": "Table",
+ "hidden": 1,
"label": "Production Plan Item Reference",
"options": "Production Plan Item Reference"
},
@@ -397,7 +398,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-26 14:51:08.774372",
+ "modified": "2023-03-31 10:30:48.118932",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
index 84dee4a..15ef207 100644
--- a/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
+++ b/erpnext/manufacturing/doctype/production_plan_item_reference/production_plan_item_reference.json
@@ -28,7 +28,7 @@
"fieldname": "qty",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "qty"
+ "label": "Qty"
},
{
"fieldname": "item_reference",
@@ -40,7 +40,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-07 17:03:49.707487",
+ "modified": "2023-03-31 10:30:14.604051",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item Reference",
@@ -48,5 +48,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 8e57ebd..8efc47d 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -135,7 +135,7 @@
}
else {
// allow for '0' qty on Credit/Debit notes
- let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1;
+ let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 8d69ea0..0bd4d91 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1897,20 +1897,60 @@
}
make_payment_entry() {
+ let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry;
+ if(this.has_discount_in_schedule() && !via_journal_entry) {
+ // If early payment discount is applied, ask user for reference date
+ this.prompt_user_for_reference_date();
+ } else {
+ this.make_mapped_payment_entry();
+ }
+ }
+
+ make_mapped_payment_entry(args) {
+ var me = this;
+ args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name };
return frappe.call({
- method: cur_frm.cscript.get_method_for_payment(),
- args: {
- "dt": cur_frm.doc.doctype,
- "dn": cur_frm.doc.name
- },
+ method: me.get_method_for_payment(),
+ args: args,
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
- // cur_frm.refresh_fields()
}
});
}
+ prompt_user_for_reference_date(){
+ var me = this;
+ frappe.prompt({
+ label: __("Cheque/Reference Date"),
+ fieldname: "reference_date",
+ fieldtype: "Date",
+ reqd: 1,
+ }, (values) => {
+ let args = {
+ "dt": me.frm.doc.doctype,
+ "dn": me.frm.doc.name,
+ "reference_date": values.reference_date
+ }
+ me.make_mapped_payment_entry(args);
+ },
+ __("Reference Date for Early Payment Discount"),
+ __("Continue")
+ );
+ }
+
+ has_discount_in_schedule() {
+ let is_eligible = in_list(
+ ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"],
+ this.frm.doctype
+ );
+ let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length;
+ if(!is_eligible || !has_payment_schedule) return false;
+
+ let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date);
+ return has_discount;
+ }
+
make_quality_inspection() {
let data = [];
const fields = [
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 489ec6e..2df39c8 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -35,7 +35,14 @@
@frappe.whitelist()
-def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True):
+def get_item_details(
+ args,
+ doc=None,
+ for_validate=False,
+ overwrite_warehouse=True,
+ return_basic_details=False,
+ basic_details=None,
+):
"""
args = {
"item_code": "",
@@ -73,7 +80,13 @@
if doc.get("doctype") == "Purchase Invoice":
args["bill_date"] = doc.get("bill_date")
- out = get_basic_details(args, item, overwrite_warehouse)
+ if not basic_details:
+ out = get_basic_details(args, item, overwrite_warehouse)
+ else:
+ out = basic_details
+
+ basic_details = out.copy()
+
get_item_tax_template(args, item, out)
out["item_tax_rate"] = get_item_tax_map(
args.company,
@@ -141,7 +154,11 @@
out.amount = flt(args.qty) * flt(out.rate)
out = remove_standard_fields(out)
- return out
+
+ if return_basic_details:
+ return out, basic_details
+ else:
+ return out
def remove_standard_fields(details):
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index d84e4c4..7eba35d 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -58,11 +58,11 @@
def compare_values(self, ref_doc, fields, doc=None):
for reference_doctype, ref_dn_list in ref_doc.items():
+ prev_doc_detail_map = self.get_prev_doc_reference_details(
+ ref_dn_list, reference_doctype, fields
+ )
for reference_name in ref_dn_list:
- prevdoc_values = frappe.db.get_value(
- reference_doctype, reference_name, [d[0] for d in fields], as_dict=1
- )
-
+ prevdoc_values = prev_doc_detail_map.get(reference_name)
if not prevdoc_values:
frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name))
@@ -70,6 +70,19 @@
if prevdoc_values[field] is not None and field not in self.exclude_fields:
self.validate_value(field, condition, prevdoc_values[field], doc)
+ def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):
+ prev_doc_detail_map = {}
+ details = frappe.get_all(
+ reference_doctype,
+ filters={"name": ("in", reference_names)},
+ fields=["name"] + [d[0] for d in fields],
+ )
+
+ for d in details:
+ prev_doc_detail_map.setdefault(d.name, d)
+
+ return prev_doc_detail_map
+
def validate_rate_with_reference_doc(self, ref_details):
if self.get("is_internal_supplier"):
return
@@ -77,23 +90,23 @@
buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
if self.doctype in buying_doctypes:
- action = frappe.db.get_single_value("Buying Settings", "maintain_same_rate_action")
- settings_doc = "Buying Settings"
+ action, role_allowed_to_override = frappe.get_cached_value(
+ "Buying Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
+ )
else:
- action = frappe.db.get_single_value("Selling Settings", "maintain_same_rate_action")
- settings_doc = "Selling Settings"
+ action, role_allowed_to_override = frappe.get_cached_value(
+ "Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
+ )
for ref_dt, ref_dn_field, ref_link_field in ref_details:
+ reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
+ reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
for d in self.get("items"):
if d.get(ref_link_field):
- ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate")
+ ref_rate = reference_details.get(d.get(ref_link_field))
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
if action == "Stop":
- role_allowed_to_override = frappe.db.get_single_value(
- settings_doc, "role_to_override_stop_action"
- )
-
if role_allowed_to_override not in frappe.get_roles():
frappe.throw(
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
@@ -109,6 +122,16 @@
indicator="orange",
)
+ def get_reference_details(self, reference_names, reference_doctype):
+ return frappe._dict(
+ frappe.get_all(
+ reference_doctype,
+ filters={"name": ("in", reference_names)},
+ fields=["name", "rate"],
+ as_list=1,
+ )
+ )
+
def get_link_filters(self, for_doctype):
if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype):
fieldname = self.prev_link_mapper[for_doctype]["fieldname"]