diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 8a50a5d369da..456ca52020ba 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -2,7 +2,7 @@
import frappe
-__version__ = "14.20.0"
+__version__ = "14.20.3"
def get_default_company(user=None):
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 1e2e2acd79af..1c0d64f065b8 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",
@@ -347,6 +348,13 @@
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account "
+ },
+ {
+ "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",
@@ -354,7 +362,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-11-27 21:49:52.538655",
+ "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 80878ac50682..081718726bdf 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -81,7 +81,7 @@ def get_payment_entries(self):
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 @@ def get_payment_entries(self):
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 @@ def get_payment_entries(self):
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 7d7392eac5ef..d475fc01172a 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
@@ -336,14 +336,14 @@ def get_template(template_type):
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/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index d77870651e9b..62916cb9d533 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -51,7 +51,7 @@ def validate(self):
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
-
+ self.set_total_debit_credit()
# Do not validate while importing via data import
if not frappe.flags.in_import:
self.validate_total_debit_and_credit()
@@ -659,7 +659,6 @@ def validate_debit_credit_amount(self):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self):
- self.set_total_debit_credit()
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
if self.difference:
frappe.throw(
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 91374ae217b2..5a56a6b00466 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -245,8 +245,6 @@ frappe.ui.form.on('Payment Entry', {
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 a585924d20fd..58ed7d1822c0 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -416,7 +416,7 @@ def update_payment_schedule(self, cancel=0):
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 @@ def update_payment_schedule(self, cancel=0):
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 @@ def update_payment_schedule(self, cancel=0):
(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 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
@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 @@ def get_payment_entry(
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 @@ def get_payment_entry(
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 @@ def get_payment_entry(
):
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 @@ def get_payment_entry(
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 @@ def set_paid_amount_and_received_amount(
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 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
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 123b5dfd512b..67049c47ad05 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 @@ def test_payment_entry_against_payment_terms_with_discount(self):
},
)
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 @@ def test_payment_entry_against_payment_terms_with_discount(self):
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 @@ def create_payment_terms_template():
).insert()
-def create_payment_terms_template_with_discount():
-
- create_payment_term("30 Credit Days with 10% 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"
- 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 61a1462dd7a4..1c31829f0ea6 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 d986f320669c..caffac5354ff 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -272,4 +272,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}
};
+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 c9e3998ac8a0..d8082d058f3f 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -233,6 +233,15 @@ def get_difference_amount(self, payment_entry, invoice, allocated_amount):
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/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index d9b07435fdb3..0955664d98be 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -497,10 +497,16 @@ def get_amount(ref_doc, payment_account=None):
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
- if ref_doc.party_account_currency == ref_doc.currency:
- grand_total = flt(ref_doc.outstanding_amount)
- else:
- grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
+ if not ref_doc.get("is_pos"):
+ if ref_doc.party_account_currency == ref_doc.currency:
+ grand_total = flt(ref_doc.outstanding_amount)
+ else:
+ grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
+ elif dt == "Sales Invoice":
+ for pay in ref_doc.payments:
+ if pay.type == "Phone" and pay.account == payment_account:
+ grand_total = pay.amount
+ break
elif dt == "POS Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 4279aa4f85c6..e17a846dd814 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -6,6 +6,7 @@
import frappe
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate
@@ -74,6 +75,29 @@ def test_payment_request_linkings(self):
self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD")
+ def test_payment_entry_against_purchase_invoice(self):
+ si_usd = make_purchase_invoice(
+ customer="_Test Supplier USD",
+ debit_to="_Test Payable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
+
+ pr = make_payment_request(
+ dt="Purchase Invoice",
+ dn=si_usd.name,
+ recipient_id="user@example.com",
+ mute_email=1,
+ payment_gateway_account="_Test Gateway - USD",
+ submit_doc=1,
+ return_doc=1,
+ )
+
+ pe = pr.create_payment_entry()
+ pr.load_from_db()
+
+ self.assertEqual(pr.status, "Paid")
+
def test_payment_entry(self):
frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 0af4c0ea4804..27e6f0e598f5 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -674,7 +674,7 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql(
- """select sum(p_item.qty) as qty
+ """select sum(p_item.stock_qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and ifnull(p.consolidated_invoice, '') = ''
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index e2b4a1ad5bef..5c9168bf9c50 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
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.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index ce331b807b46..5ae2e148d807 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -119,6 +119,7 @@
"paid_amount",
"advances_section",
"allocate_advances_automatically",
+ "only_include_allocated_payments",
"get_advances",
"advances",
"advance_tax",
@@ -1554,6 +1555,13 @@
},
{
"default": "0",
+ "depends_on": "allocate_advances_automatically",
+ "description": "Advance payments allocated against orders will only be fetched",
+ "fieldname": "only_include_allocated_payments",
+ "fieldtype": "Check",
+ "label": "Only Include Allocated Payments"
+ },
+ {
"depends_on": "eval: doc.supplier",
"fieldname": "filter_items_by_supplier",
"fieldtype": "Check",
@@ -1564,11 +1572,10 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2023-01-28 19:18:56.586321",
+ "modified": "2023-04-03 22:57:14.074982",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
- "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index ae707ab14355..0ded4a883bb9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -117,7 +117,7 @@ def validate(self):
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 @@ def validate_with_previous_doc(self):
)
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 @@ def get_gl_entries(self, warehouse_account=None):
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 @@ def make_item_gl_entries(self, gl_entries):
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 47e3f9b93548..56e412b297c9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
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.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 9a5b42be4bb7..9a0d71a3850b 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -120,6 +120,7 @@
"account_for_change_amount",
"advances_section",
"allocate_advances_automatically",
+ "only_include_allocated_payments",
"get_advances",
"advances",
"write_off_section",
@@ -2126,6 +2127,13 @@
"label": "Repost Required",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "depends_on": "allocate_advances_automatically",
+ "description": "Advance payments allocated against orders will only be fetched",
+ "fieldname": "only_include_allocated_payments",
+ "fieldtype": "Check",
+ "label": "Only Include Allocated Payments"
}
],
"icon": "fa fa-file-text",
@@ -2138,7 +2146,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2023-03-13 11:43:15.883055",
+ "modified": "2023-04-03 22:55:14.206473",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f5be4c7a3f3e..7af98ddf9340 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -145,7 +145,7 @@ def validate(self):
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/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
index 57d80492ae0e..f21c94b4940c 100644
--- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
+++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
@@ -25,6 +25,7 @@ def get_data(filters):
["posting_date", "<=", filters.get("to_date")],
["against_voucher_type", "=", "Asset"],
["account", "in", depreciation_accounts],
+ ["is_cancelled", "=", 0],
]
if filters.get("asset"):
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 449ebdcd9240..306af722ba84 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 @@ def get_entries(filters):
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/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 21d846f68062..0923d0093f97 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -469,6 +469,9 @@ frappe.ui.form.on('Asset', {
} else {
frm.set_value('purchase_date', purchase_doc.posting_date);
}
+ if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
+ frm.set_value('available_for_use_date', frm.doc.purchase_date);
+ }
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
doctype_field = frappe.scrub(doctype)
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index d581f52153e7..3e93f0f03e3c 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -81,6 +81,9 @@
"options": "ACC-ASS-.YYYY.-"
},
{
+ "depends_on": "item_code",
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
"fieldname": "asset_name",
"fieldtype": "Data",
"in_list_view": 1,
@@ -527,7 +530,7 @@
"table_fieldname": "accounts"
}
],
- "modified": "2023-01-25 17:45:48.649543",
+ "modified": "2023-03-30 15:07:41.542374",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -571,4 +574,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 47b5f75e6686..fe1fd98aa098 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -294,17 +294,42 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
if has_pro_rata:
number_of_pending_depreciations += 1
+ has_wdv_or_dd_non_yearly_pro_rata = False
+ if (
+ finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance")
+ and cint(finance_book.frequency_of_depreciation) != 12
+ ):
+ has_wdv_or_dd_non_yearly_pro_rata = self.check_is_pro_rata(
+ finance_book, wdv_or_dd_non_yearly=True
+ )
+
skip_row = False
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
+ depreciation_amount = 0
+
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
- depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
+ if n > 0 and len(self.get("schedules")) > n - 1:
+ prev_depreciation_amount = self.get("schedules")[n - 1].depreciation_amount
+ else:
+ prev_depreciation_amount = 0
+
+ depreciation_amount = get_depreciation_amount(
+ self,
+ value_after_depreciation,
+ finance_book,
+ n,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ )
- if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
+ if not has_pro_rata or (
+ n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
+ ):
schedule_date = add_months(
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
)
@@ -320,7 +345,10 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
if date_of_disposal:
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(
- finance_book, depreciation_amount, from_date, date_of_disposal
+ finance_book,
+ depreciation_amount,
+ from_date,
+ date_of_disposal,
)
if depreciation_amount > 0:
@@ -335,12 +363,20 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
break
# For first row
- if has_pro_rata and not self.opening_accumulated_depreciation and n == 0:
+ if (
+ (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
+ and not self.opening_accumulated_depreciation
+ and n == 0
+ ):
from_date = add_days(
self.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(
- finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date
+ finance_book,
+ depreciation_amount,
+ from_date,
+ finance_book.depreciation_start_date,
+ has_wdv_or_dd_non_yearly_pro_rata,
)
# For first depr schedule date will be the start date
@@ -359,7 +395,11 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(
- finance_book, depreciation_amount, schedule_date, self.to_date
+ finance_book,
+ depreciation_amount,
+ schedule_date,
+ self.to_date,
+ has_wdv_or_dd_non_yearly_pro_rata,
)
depreciation_amount = self.get_adjusted_depreciation_amount(
@@ -479,28 +519,37 @@ def get_from_date(self, finance_book):
return add_days(self.available_for_use_date, -1)
# if it returns True, depreciation_amount will not be equal for the first and last rows
- def check_is_pro_rata(self, row):
+ def check_is_pro_rata(self, row, wdv_or_dd_non_yearly=False):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
- from_date = self.get_modified_available_for_use_date(row)
+ from_date = self.get_modified_available_for_use_date(row, wdv_or_dd_non_yearly)
days = date_diff(row.depreciation_start_date, from_date) + 1
- # if frequency_of_depreciation is 12 months, total_days = 365
- total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
+ if wdv_or_dd_non_yearly:
+ total_days = get_total_days(row.depreciation_start_date, 12)
+ else:
+ # if frequency_of_depreciation is 12 months, total_days = 365
+ total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
- def get_modified_available_for_use_date(self, row):
- return add_months(
- self.available_for_use_date,
- (self.number_of_depreciations_booked * row.frequency_of_depreciation),
- )
+ def get_modified_available_for_use_date(self, row, wdv_or_dd_non_yearly=False):
+ if wdv_or_dd_non_yearly:
+ return add_months(
+ self.available_for_use_date,
+ (self.number_of_depreciations_booked * 12),
+ )
+ else:
+ return add_months(
+ self.available_for_use_date,
+ (self.number_of_depreciations_booked * row.frequency_of_depreciation),
+ )
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
@@ -903,7 +952,12 @@ def get_depreciation_rate(self, args, on_validate=False):
float_precision = cint(frappe.db.get_default("float_precision")) or 2
if args.get("depreciation_method") == "Double Declining Balance":
- return 200.0 / args.get("total_number_of_depreciations")
+ return 200.0 / (
+ (
+ flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation"))
+ )
+ / 12
+ )
if args.get("depreciation_method") == "Written Down Value":
if (
@@ -920,14 +974,29 @@ def get_depreciation_rate(self, args, on_validate=False):
else:
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
- depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
+ depreciation_rate = math.pow(
+ value,
+ 1.0
+ / (
+ (
+ flt(args.get("total_number_of_depreciations"), 2)
+ * flt(args.get("frequency_of_depreciation"))
+ )
+ / 12
+ ),
+ )
return flt((100 * (1 - depreciation_rate)), float_precision)
- def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
+ def get_pro_rata_amt(
+ self, row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False
+ ):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
- total_days = get_total_days(to_date, row.frequency_of_depreciation)
+ if has_wdv_or_dd_non_yearly_pro_rata:
+ total_days = get_total_days(to_date, 12)
+ else:
+ total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
@@ -1184,27 +1253,72 @@ def get_total_days(date, frequency):
@erpnext.allow_regional
-def get_depreciation_amount(asset, depreciable_value, row):
+def get_depreciation_amount(
+ asset,
+ depreciable_value,
+ row,
+ schedule_idx=0,
+ prev_depreciation_amount=0,
+ has_wdv_or_dd_non_yearly_pro_rata=False,
+):
if row.depreciation_method in ("Straight Line", "Manual"):
- # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
- if asset.flags.increase_in_asset_life:
- depreciation_amount = (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
- # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
- elif asset.flags.increase_in_asset_value_due_to_repair:
- depreciation_amount = (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
- # if the Depreciation Schedule is being prepared for the first time
- else:
- depreciation_amount = (
- flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
+ return get_straight_line_or_manual_depr_amount(asset, row)
else:
- depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
+ return get_wdv_or_dd_depr_amount(
+ depreciable_value,
+ row.rate_of_depreciation,
+ row.frequency_of_depreciation,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+ )
+
- return depreciation_amount
+def get_straight_line_or_manual_depr_amount(asset, row):
+ # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
+ if asset.flags.increase_in_asset_life:
+ return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
+ date_diff(asset.to_date, asset.available_for_use_date) / 365
+ )
+ # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
+ elif asset.flags.increase_in_asset_value_due_to_repair:
+ return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
+ row.total_number_of_depreciations
+ )
+ # if the Depreciation Schedule is being prepared for the first time
+ else:
+ return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
+ row.total_number_of_depreciations
+ )
+
+
+def get_wdv_or_dd_depr_amount(
+ depreciable_value,
+ rate_of_depreciation,
+ frequency_of_depreciation,
+ schedule_idx,
+ prev_depreciation_amount,
+ has_wdv_or_dd_non_yearly_pro_rata,
+):
+ if cint(frequency_of_depreciation) == 12:
+ return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
+ else:
+ if has_wdv_or_dd_non_yearly_pro_rata:
+ if schedule_idx == 0:
+ return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
+ elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
+ return (
+ flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
+ )
+ else:
+ return prev_depreciation_amount
+ else:
+ if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
+ return (
+ flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
+ )
+ else:
+ return prev_depreciation_amount
@frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index e72e0afb9ce4..74625890a697 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -218,10 +218,16 @@ def notify_depr_entry_posting_error(failed_asset_names):
asset_links = get_comma_separated_asset_links(failed_asset_names)
message = (
- _("Hi,")
- + "
"
- + _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
+ _("Hello,")
+ + "
"
+ + _("The following assets have failed to automatically post depreciation entries: {0}").format(
+ asset_links
+ )
+ "."
+ + "
"
+ + _(
+ "Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table."
+ )
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 71f578c67030..7eaa4bf997a8 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -818,12 +818,12 @@ def test_monthly_depreciation_by_wdv_method(self):
)
expected_schedules = [
- ["2022-02-28", 647.25, 647.25],
- ["2022-03-31", 1210.71, 1857.96],
- ["2022-04-30", 1053.99, 2911.95],
- ["2022-05-31", 917.55, 3829.5],
- ["2022-06-30", 798.77, 4628.27],
- ["2022-07-15", 371.73, 5000.0],
+ ["2022-02-28", 310.89, 310.89],
+ ["2022-03-31", 654.45, 965.34],
+ ["2022-04-30", 654.45, 1619.79],
+ ["2022-05-31", 654.45, 2274.24],
+ ["2022-06-30", 654.45, 2928.69],
+ ["2022-07-15", 2071.31, 5000.0],
]
schedules = [
diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
index 0028d84508d8..83031415ec3d 100644
--- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
+++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py
@@ -84,6 +84,8 @@ def calculate_next_due_date(
next_due_date = add_years(start_date, 1)
if periodicity == "2 Yearly":
next_due_date = add_years(start_date, 2)
+ if periodicity == "3 Yearly":
+ next_due_date = add_years(start_date, 3)
if periodicity == "Quarterly":
next_due_date = add_months(start_date, 3)
if end_date and (
diff --git a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json
index 20963e3fdc7a..b7cb23e66878 100644
--- a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json
+++ b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json
@@ -1,664 +1,156 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-10-20 07:10:55.903571",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-10-20 07:10:55.903571",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "maintenance_task",
+ "maintenance_type",
+ "column_break_2",
+ "maintenance_status",
+ "section_break_2",
+ "start_date",
+ "periodicity",
+ "column_break_4",
+ "end_date",
+ "certificate_required",
+ "section_break_9",
+ "assign_to",
+ "column_break_10",
+ "assign_to_name",
+ "section_break_10",
+ "next_due_date",
+ "column_break_14",
+ "last_completion_date",
+ "section_break_7",
+ "description"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maintenance_task",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Maintenance Task",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "maintenance_task",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Maintenance Task",
+ "reqd": 1
+ },
+ {
+ "fieldname": "maintenance_type",
+ "fieldtype": "Select",
+ "label": "Maintenance Type",
+ "options": "Preventive Maintenance\nCalibration"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "maintenance_status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Maintenance Status",
+ "options": "Planned\nOverdue\nCancelled",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "Today",
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "label": "Start Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "periodicity",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Periodicity",
+ "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "end_date",
+ "fieldtype": "Date",
+ "label": "End Date"
+ },
+ {
+ "default": "0",
+ "fieldname": "certificate_required",
+ "fieldtype": "Check",
+ "label": "Certificate Required",
+ "search_index": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "assign_to",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Assign To",
+ "options": "User"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maintenance_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Maintenance Type",
- "length": 0,
- "no_copy": 0,
- "options": "Preventive Maintenance\nCalibration",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "maintenance_status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Maintenance Status",
- "length": 0,
- "no_copy": 0,
- "options": "Planned\nOverdue\nCancelled",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Today",
- "fieldname": "start_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Start Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "periodicity",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Periodicity",
- "length": 0,
- "no_copy": 0,
- "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "end_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "End Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "certificate_required",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Certificate Required",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_9",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "assign_to",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Assign To",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "assign_to.full_name",
- "fieldname": "assign_to_name",
- "fieldtype": "Read Only",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Assign to Name",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_10",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "next_due_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Next Due Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_14",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "last_completion_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Last Completion Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_7",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "assign_to_name",
+ "fieldtype": "Read Only",
+ "label": "Assign to Name"
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "next_due_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Next Due Date"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "last_completion_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Last Completion Date"
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-06-18 16:12:04.330021",
- "modified_by": "Administrator",
- "module": "Assets",
- "name": "Asset Maintenance Task",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2023-03-23 07:03:07.113452",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Maintenance Task",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index ae0e1bda0204..d07f40cdf422 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
frm.call({
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
args: {
- asset: frm.doc.asset,
+ asset_name: frm.doc.asset,
finance_book: frm.doc.finance_book
},
callback: function(r) {
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 47089f7d8503..c6c9f1f98a39 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
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/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 7927beb8233b..4590f8c3d937 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -113,7 +113,10 @@ def send_to_supplier(self):
def get_link(self):
# RFQ link for supplier portal
- return get_url("/app/request-for-quotation/" + self.name)
+ route = frappe.db.get_value(
+ "Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
+ )
+ return get_url("/app/{0}/".format(route) + self.name)
def update_supplier_part_no(self, supplier):
self.vendor = supplier
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 1b7f4b3f47d9..8e2e6e83a8b6 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -511,6 +511,8 @@ def set_missing_item_details(self, for_validate=False):
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()
@@ -529,7 +531,17 @@ def set_missing_item_details(self, for_validate=False):
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:
@@ -829,7 +841,9 @@ def get_shipping_address(self):
def set_advances(self):
"""Returns list of advances against Account, Party, Reference"""
- res = self.get_advance_entries()
+ res = self.get_advance_entries(
+ include_unallocated=not cint(self.get("only_include_allocated_payments"))
+ )
self.set("advances", [])
advance_allocated = 0
@@ -1228,7 +1242,7 @@ def make_discount_gl_entries(self, gl_entries):
)
)
- 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 = {}
@@ -1241,17 +1255,20 @@ def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
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"),
@@ -1298,6 +1315,16 @@ def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
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/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py
index 8660c7331038..7b8c43b2d65f 100644
--- a/erpnext/crm/report/lead_details/lead_details.py
+++ b/erpnext/crm/report/lead_details/lead_details.py
@@ -98,7 +98,7 @@ def get_data(filters):
`tabAddress`.name=`tabDynamic Link`.parent)
WHERE
company = %(company)s
- AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s
+ AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
{conditions}
ORDER BY
`tabLead`.creation asc """.format(
diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py
index 254511c92fa2..b37cfa449fee 100644
--- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py
+++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py
@@ -82,7 +82,7 @@ def get_data(filters):
{join}
WHERE
`tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s
- AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s
+ AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s
{conditions}
GROUP BY
`tabOpportunity`.name
diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py
index bbe04d5514d8..019a5f9ee4f4 100644
--- a/erpnext/e_commerce/doctype/website_item/test_website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py
@@ -199,8 +199,14 @@ def test_website_item_breadcrumbs(self):
breadcrumbs = get_parent_item_groups(item.item_group)
+ settings = frappe.get_cached_doc("E Commerce Settings")
+ if settings.enable_field_filters:
+ base_breadcrumb = "Shop by Category"
+ else:
+ base_breadcrumb = "All Products"
+
self.assertEqual(breadcrumbs[0]["name"], "Home")
- self.assertEqual(breadcrumbs[1]["name"], "All Products")
+ self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb)
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index f3aa6a379355..e57a30a88e18 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -220,7 +220,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
if e.code == "ITEM_LOGIN_REQUIRED":
msg = _("There was an error syncing transactions.") + " "
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
- frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
+ frappe.log_error(message=msg, title=_("Plaid Link Refresh Required"))
return transactions
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index f1ee370e97ee..c4032596f47a 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -30,6 +30,10 @@
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
+override_whitelisted_methods = {
+ "frappe.www.contact.send_message": "erpnext.templates.utils.send_message"
+}
+
welcome_email = "erpnext.setup.utils.welcome_email"
# setup wizard
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 619a415c8bc9..a085af859a4b 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -943,7 +943,8 @@ def get_valuation_rate(data):
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 @@ def get_valuation_rate(data):
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/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index 51f7b24e745f..7477f9528ecb 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -164,7 +164,7 @@ def queue_bom_cost_jobs(
while current_boms_list:
batch_no += 1
- batch_size = 20_000
+ batch_size = 7_000
boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs
# update list to exclude 20K (queued) BOMs
@@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs():
["name", "boms_updated", "status"],
)
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
- if not bom_batches or not incomplete_level:
+ if not bom_batches or incomplete_level:
continue
# Prep parent BOMs & updated processed BOMs for next level
@@ -252,9 +252,6 @@ def get_processed_current_boms(
current_boms = []
for row in bom_batches:
- if not row.boms_updated:
- continue
-
boms_updated = json.loads(row.boms_updated)
current_boms.extend(boms_updated)
boms_updated_dict = {bom: True for bom in boms_updated}
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 2624daa41e2d..fdaa4a2a1d46 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 84dee4ad284c..15ef20794cb8 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/accounts.js b/erpnext/public/js/controllers/accounts.js
index a07f75d1c5d4..d943126018a6 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -55,6 +55,14 @@ frappe.ui.form.on(cur_frm.doctype, {
},
allocate_advances_automatically: function(frm) {
+ frm.trigger('fetch_advances');
+ },
+
+ only_include_allocated_payments: function(frm) {
+ frm.trigger('fetch_advances');
+ },
+
+ fetch_advances: function(frm) {
if(frm.doc.allocate_advances_automatically) {
frappe.call({
doc: frm.doc,
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 8e57ebd36774..8efc47d18e5d 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -135,7 +135,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
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 f7620db2f1e4..07d1955bfafd 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1897,20 +1897,60 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
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/public/js/website_utils.js b/erpnext/public/js/website_utils.js
index b5416065d791..2bb5255eebc7 100644
--- a/erpnext/public/js/website_utils.js
+++ b/erpnext/public/js/website_utils.js
@@ -3,18 +3,6 @@
if(!window.erpnext) window.erpnext = {};
-// Add / update a new Lead / Communication
-// subject, sender, description
-frappe.send_message = function(opts, btn) {
- return frappe.call({
- type: "POST",
- method: "erpnext.templates.utils.send_message",
- btn: btn,
- args: opts,
- callback: opts.callback
- });
-};
-
erpnext.subscribe_to_newsletter = function(opts, btn) {
return frappe.call({
type: "POST",
@@ -24,6 +12,3 @@ erpnext.subscribe_to_newsletter = function(opts, btn) {
callback: opts.callback
});
}
-
-// for backward compatibility
-erpnext.send_message = frappe.send_message;
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 2fdfcf647d01..2eca5cad8e2e 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -148,12 +148,17 @@ def get_item_for_list_in_html(context):
def get_parent_item_groups(item_group_name, from_item=False):
- base_nav_page = {"name": _("All Products"), "route": "/all-products"}
+ settings = frappe.get_cached_doc("E Commerce Settings")
+
+ if settings.enable_field_filters:
+ base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"}
+ else:
+ base_nav_page = {"name": _("All Products"), "route": "/all-products"}
if from_item and frappe.request.environ.get("HTTP_REFERER"):
# base page after 'Home' will vary on Item page
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
- if last_page and last_page == "shop-by-category":
+ if last_page and last_page in ("shop-by-category", "all-products"):
base_nav_page_title = " ".join(last_page.split("-")).title()
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index f14288beb20f..4a165212dcec 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname, revert_series_if_last
-from frappe.utils import cint, flt, get_link_to_form
+from frappe.utils import cint, flt, get_link_to_form, nowtime
from frappe.utils.data import add_days
from frappe.utils.jinja import render_template
@@ -179,7 +179,11 @@ def get_batch_qty(
out = 0
if batch_no and warehouse:
cond = ""
- if posting_date and posting_time:
+
+ if posting_date:
+ if posting_time is None:
+ posting_time = nowtime()
+
cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format(
posting_date, posting_time
)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 3f6a2c881b82..482b103d1e4b 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -4,7 +4,8 @@
from typing import Optional
import frappe
-from frappe import _, msgprint
+from frappe import _, bold, msgprint
+from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt
import erpnext
@@ -89,7 +90,7 @@ def _changed(item):
if item_dict.get("serial_nos"):
item.current_serial_no = item_dict.get("serial_nos")
- if self.purpose == "Stock Reconciliation" and not item.serial_no:
+ if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty:
item.serial_no = item.current_serial_no
item.current_qty = item_dict.get("qty")
@@ -140,6 +141,14 @@ def _get_msg(row_num, msg):
self.validate_item(row.item_code, row)
+ if row.serial_no and not row.qty:
+ self.validation_messages.append(
+ _get_msg(
+ row_num,
+ f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified",
+ )
+ )
+
# validate warehouse
if not frappe.db.get_value("Warehouse", row.warehouse):
self.validation_messages.append(_get_msg(row_num, _("Warehouse not found in the system")))
@@ -561,6 +570,54 @@ def cancel(self):
else:
self._cancel()
+ def recalculate_current_qty(self, item_code, batch_no):
+ for row in self.items:
+ if not (row.item_code == item_code and row.batch_no == batch_no):
+ continue
+
+ row.current_qty = get_batch_qty_for_stock_reco(item_code, row.warehouse, batch_no)
+
+ qty, val_rate = get_stock_balance(
+ item_code,
+ row.warehouse,
+ self.posting_date,
+ self.posting_time,
+ with_valuation_rate=True,
+ )
+
+ row.current_valuation_rate = val_rate
+
+ row.db_set(
+ {
+ "current_qty": row.current_qty,
+ "current_valuation_rate": row.current_valuation_rate,
+ "current_amount": flt(row.current_qty * row.current_valuation_rate),
+ }
+ )
+
+
+def get_batch_qty_for_stock_reco(item_code, warehouse, batch_no):
+ ledger = frappe.qb.DocType("Stock Ledger Entry")
+
+ query = (
+ frappe.qb.from_(ledger)
+ .select(
+ Sum(ledger.actual_qty).as_("batch_qty"),
+ )
+ .where(
+ (ledger.item_code == item_code)
+ & (ledger.warehouse == warehouse)
+ & (ledger.docstatus == 1)
+ & (ledger.is_cancelled == 0)
+ & (ledger.batch_no == batch_no)
+ )
+ .groupby(ledger.batch_no)
+ )
+
+ sle = query.run(as_dict=True)
+
+ return flt(sle[0].batch_qty) if sle else 0
+
@frappe.whitelist()
def get_items(
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 489ec6ebecce..2df39c818326 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 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
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 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
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/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index da17cdeb5aed..77bc4e004de7 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -34,6 +34,9 @@ def execute(filters=None):
conversion_factors.append(0)
actual_qty = stock_value = 0
+ if opening_row:
+ actual_qty = opening_row.get("qty_after_transaction")
+ stock_value = opening_row.get("stock_value")
available_serial_nos = {}
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 08fc6fbd42fb..c954befdc297 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -1337,6 +1337,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
next_stock_reco_detail = get_next_stock_reco(args)
if next_stock_reco_detail:
detail = next_stock_reco_detail[0]
+ if detail.batch_no:
+ regenerate_sle_for_batch_stock_reco(detail)
+
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail)
@@ -1364,6 +1367,16 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
validate_negative_qty_in_future_sle(args, allow_negative_stock)
+def regenerate_sle_for_batch_stock_reco(detail):
+ doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
+ doc.docstatus = 2
+ doc.update_stock_ledger()
+
+ doc.recalculate_current_qty(detail.item_code, detail.batch_no)
+ doc.docstatus = 1
+ doc.update_stock_ledger()
+
+
def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = 0
if args.get("is_cancelled"):
@@ -1393,7 +1406,7 @@ def get_next_stock_reco(args):
return frappe.db.sql(
"""
select
- name, posting_date, posting_time, creation, voucher_no
+ name, posting_date, posting_time, creation, voucher_no, item_code, batch_no, actual_qty
from
`tabStock Ledger Entry`
where
diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py
index 48b44802a8f8..57750a56f6f9 100644
--- a/erpnext/templates/utils.py
+++ b/erpnext/templates/utils.py
@@ -6,13 +6,12 @@
@frappe.whitelist(allow_guest=True)
-def send_message(subject="Website Query", message="", sender="", status="Open"):
+def send_message(sender, message, subject="Website Query"):
from frappe.www.contact import send_message as website_send_message
- lead = customer = None
-
- website_send_message(subject, message, sender)
+ website_send_message(sender, message, subject)
+ lead = customer = None
customer = frappe.db.sql(
"""select distinct dl.link_name from `tabDynamic Link` dl
left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer'
@@ -58,5 +57,3 @@ def send_message(subject="Website Query", message="", sender="", status="Open"):
}
)
comm.insert(ignore_permissions=True)
-
- return "okay"
diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py
index 21a0a551b627..7eba35dedd93 100644
--- a/erpnext/utilities/transaction_base.py
+++ b/erpnext/utilities/transaction_base.py
@@ -58,11 +58,11 @@ def validate_with_previous_doc(self, ref):
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 @@ def compare_values(self, ref_doc, fields, doc=None):
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 @@ def validate_rate_with_reference_doc(self, ref_details):
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 @@ def validate_rate_with_reference_doc(self, ref_details):
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"]
@@ -186,12 +209,15 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
for f in qty_fields:
qty = d.get(f)
if qty:
- if abs(cint(qty) - flt(qty)) > 0.0000001:
+ if abs(cint(qty) - flt(qty, d.precision(f))) > 0.0000001:
frappe.throw(
_(
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}."
).format(
- qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))
+ flt(qty, d.precision(f)),
+ d.idx,
+ frappe.bold(_("Must be Whole Number")),
+ frappe.bold(d.get(uom_field)),
),
UOMMustBeIntegerError,
)
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
index 219747c9f8a5..913c1836acdf 100644
--- a/erpnext/www/shop-by-category/index.py
+++ b/erpnext/www/shop-by-category/index.py
@@ -53,6 +53,7 @@ def get_tabs(categories):
def get_category_records(categories: list):
categorical_data = {}
+ website_item_meta = frappe.get_meta("Website Item", cached=True)
for c in categories:
if c == "item_group":
@@ -64,7 +65,16 @@ def get_category_records(categories: list):
continue
- doctype = frappe.unscrub(c)
+ field_type = website_item_meta.get_field(c).fieldtype
+
+ if field_type == "Table MultiSelect":
+ child_doc = website_item_meta.get_field(c).options
+ for field in frappe.get_meta(child_doc, cached=True).fields:
+ if field.fieldtype == "Link" and field.reqd:
+ doctype = field.options
+ else:
+ doctype = website_item_meta.get_field(c).options
+
fields = ["name"]
try: