From 0d9741fdd77be8621e5194f1391fc3d3d2c5a515 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Sep 2024 12:59:46 +0530 Subject: [PATCH 01/40] refactor: enable no-copy on Purchase Invoice status --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 3751c027c975..c2ee4166ed16 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1271,6 +1271,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", + "no_copy": 1, "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1 }, @@ -1630,7 +1631,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2024-07-25 19:42:36.931278", + "modified": "2024-09-11 12:59:19.130593", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", From 1657a83151fb002e0606c36923e722aa638cc6f2 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 12 Sep 2024 10:36:32 +0530 Subject: [PATCH 02/40] fix: typo with po_date when creating remarks (cherry picked from commit a55502e0f14133d8e6dfca9e0741c07d20056b08) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 2 +- .../doctype/sales_invoice/test_sales_invoice.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 12e26623ad00..1214ddee8cd2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -951,7 +951,7 @@ def add_remarks(self): if self.po_no: self.remarks = _("Against Customer Order {0}").format(self.po_no) if self.po_date: - self.remarks += " " + _("dated {0}").format(formatdate(self.po_data)) + self.remarks += " " + _("dated {0}").format(formatdate(self.po_date)) else: self.remarks = _("No Remarks") diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index beb347e07dba..7ac0d34e671c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,7 +8,7 @@ from frappe import qb from frappe.model.dynamic_links import get_dynamic_link_map from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, getdate, nowdate, today +from frappe.utils import add_days, flt, format_date, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -3997,6 +3997,14 @@ def test_common_party_with_foreign_currency_jv(self): self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) + def test_invoice_remarks(self): + si = frappe.copy_doc(test_records[0]) + si.po_no = "Test PO" + si.po_date = nowdate() + si.save() + si.submit() + self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}") + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From 45ff8fa296b47c809ecb643a4b67fc775d9e9848 Mon Sep 17 00:00:00 2001 From: Vishakh Desai Date: Wed, 11 Sep 2024 14:21:48 +0530 Subject: [PATCH 03/40] fix: set party_type null when payment_type is changed to Internal Transfer (cherry picked from commit 502cf0eb8d8001925b5c00583083a8f29a2d2115) --- .../accounts/doctype/payment_entry/payment_entry.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index b50dcd217e7c..f46c782112c5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -385,7 +385,15 @@ frappe.ui.form.on("Payment Entry", { payment_type: function (frm) { if (frm.doc.payment_type == "Internal Transfer") { $.each( - ["party", "party_balance", "paid_from", "paid_to", "references", "total_allocated_amount"], + [ + "party", + "party_type", + "party_balance", + "paid_from", + "paid_to", + "references", + "total_allocated_amount", + ], function (i, field) { frm.set_value(field, null); } From 5789de25b94258c8e521885cde47c5706632cc98 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Thu, 12 Sep 2024 15:25:54 +0530 Subject: [PATCH 04/40] fix: delete exchange gain loss journal entry while deleting payment entry (cherry picked from commit 9886cf0d46f68caec45467d5dbcd018084a65088) --- erpnext/accounts/utils.py | 98 +++++++++++++++------- erpnext/controllers/accounts_controller.py | 5 ++ 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index e462f749b54e..32eeb1e07a66 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -745,40 +745,74 @@ def cancel_exchange_gain_loss_journal( Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: - journals = frappe.db.get_all( - "Journal Entry Account", - filters={ - "reference_type": parent_doc.doctype, - "reference_name": parent_doc.name, - "docstatus": 1, - }, - fields=["parent"], - as_list=1, + gain_loss_journals = get_linked_exchange_gain_loss_journal( + referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=1 ) - - if journals: - gain_loss_journals = frappe.db.get_all( - "Journal Entry", - filters={ - "name": ["in", [x[0] for x in journals]], - "voucher_type": "Exchange Gain Or Loss", - "docstatus": 1, - }, - as_list=1, - ) - for doc in gain_loss_journals: - gain_loss_je = frappe.get_doc("Journal Entry", doc[0]) - if referenced_dt and referenced_dn: - references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] - if ( - len(references) == 2 - and (referenced_dt, referenced_dn) in references - and (parent_doc.doctype, parent_doc.name) in references - ): - # only cancel JE generated against parent_doc and referenced_dn - gain_loss_je.cancel() - else: + for doc in gain_loss_journals: + gain_loss_je = frappe.get_doc("Journal Entry", doc) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only cancel JE generated against parent_doc and referenced_dn gain_loss_je.cancel() + else: + gain_loss_je.cancel() + + +def delete_exchange_gain_loss_journal( + parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None +) -> None: + """ + Delete Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. + """ + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + gain_loss_journals = get_linked_exchange_gain_loss_journal( + referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=2 + ) + for doc in gain_loss_journals: + gain_loss_je = frappe.get_doc("Journal Entry", doc) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only delete JE generated against parent_doc and referenced_dn + gain_loss_je.delete() + else: + gain_loss_je.delete() + + +def get_linked_exchange_gain_loss_journal(referenced_dt: str, referenced_dn: str, je_docstatus: int) -> list: + """ + Get all the linked exchange gain/loss journal entries for a given document. + """ + gain_loss_journals = [] + if journals := frappe.db.get_all( + "Journal Entry Account", + { + "reference_type": referenced_dt, + "reference_name": referenced_dn, + "docstatus": je_docstatus, + }, + pluck="parent", + ): + gain_loss_journals = frappe.db.get_all( + "Journal Entry", + { + "name": ["in", journals], + "voucher_type": "Exchange Gain Or Loss", + "is_system_generated": 1, + "docstatus": je_docstatus, + }, + pluck="name", + ) + return gain_loss_journals def cancel_common_party_journal(self): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 49198be9b484..92efa5168f3b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -346,12 +346,17 @@ def _remove_references_in_repost_doctypes(self): repost_doc.save(ignore_permissions=True) def on_trash(self): + from erpnext.accounts.utils import delete_exchange_gain_loss_journal + self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() self.remove_serial_and_batch_bundle() # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): + # delete linked exchange gain/loss journal + delete_exchange_gain_loss_journal(self) + ple = frappe.qb.DocType("Payment Ledger Entry") frappe.qb.from_(ple).delete().where( (ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) From d618c9a481270515922a493867c68ea61354e9ac Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Thu, 12 Sep 2024 15:26:55 +0530 Subject: [PATCH 05/40] test: add unit test for deletion of gain loss jv while deleting payment entry (cherry picked from commit 7855d3034b1cc8f6385548643a8820293dfd33eb) --- .../payment_entry/test_payment_entry.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index cc03dc260bbd..771c91a462ce 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1791,6 +1791,79 @@ def test_opening_flag_for_advance_as_liability(self): # 'Is Opening' should always be 'No' for normal advance payments self.assertEqual(gl_with_opening_set, []) + @change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1}) + def test_delete_linked_exchange_gain_loss_journal(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=83.970000000, + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # create a payment entry for the invoice + pe = get_payment_entry("Sales Invoice", si.name) + pe.reference_no = "1" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 100 + pe.source_exchange_rate = 90 + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 2710, + }, + ) + pe.save() + pe.submit() + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + {"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1}, + pluck="parent", + ) + self.assertTrue(jv) + + # check cancellation of payment entry and journal entry + pe.cancel() + self.assertTrue(pe.docstatus == 2) + self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2) + + # check deletion of payment entry and journal entry + pe.delete() + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From 055e7820c85cc9aafb6dff38a22ecc76cef2eac8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Sep 2024 16:48:25 +0530 Subject: [PATCH 06/40] refactor(test): use test fixture on pricing rule test suite (cherry picked from commit 0ea1d6d9605e31b8af539ec37bc46b64c67ef5c9) --- erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 235fddf3ab39..3fece4aeeab8 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase, change_settings 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 @@ -14,7 +15,7 @@ from erpnext.stock.get_item_details import get_item_details -class TestPricingRule(unittest.TestCase): +class TestPricingRule(FrappeTestCase): def setUp(self): delete_existing_pricing_rules() setup_pricing_rule_data() From c1a6c56217b326f4c948145c507c87bbc987bafc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:29:58 +0530 Subject: [PATCH 07/40] fix: item list view in website (backport #43165) (#43207) fix: item list view in website (#43165) (cherry picked from commit ce34bb97930ea97d4abe4ac9c3227654b5d00a62) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/item/templates/item_row.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/templates/item_row.html b/erpnext/stock/doctype/item/templates/item_row.html index f81fc1d8743a..809ac838ccb0 100644 --- a/erpnext/stock/doctype/item/templates/item_row.html +++ b/erpnext/stock/doctype/item/templates/item_row.html @@ -1,4 +1,4 @@ From 61a42ea5d71b9f28c73d9954168bc3e9ba37a3cc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:30:12 +0530 Subject: [PATCH 08/40] fix: batch based item price not working (backport #43172) (#43206) * fix: batch based item price not working (#43172) (cherry picked from commit d9e4ed13cbbae7d318038554e486b67cf0b9ff7c) # Conflicts: # erpnext/stock/get_item_details.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- erpnext/public/js/controllers/transaction.js | 25 ++++++++++++++++++++ erpnext/stock/get_item_details.py | 25 +++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3dcd36b2cffc..435ce26c5c1c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1501,6 +1501,31 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + batch_no(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.use_serial_batch_fields && row.batch_no) { + var params = this._get_args(row); + params.batch_no = row.batch_no; + params.uom = row.uom; + + frappe.call({ + method: "erpnext.stock.get_item_details.get_batch_based_item_price", + args: { + params: params, + item_code: row.item_code, + }, + callback: function(r) { + if (!r.exc && r.message) { + row.price_list_rate = r.message; + row.rate = r.message; + refresh_field("rate", row.name, row.parentfield); + refresh_field("price_list_rate", row.name, row.parentfield); + } + } + }) + } + } + toggle_item_grid_columns(company_currency) { const me = this; // toggle columns diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 5cfa306066d8..50db899433fb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -10,7 +10,7 @@ from frappe.model.meta import get_field_precision from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum -from frappe.utils import add_days, add_months, cint, cstr, flt, getdate +from frappe.utils import add_days, add_months, cint, cstr, flt, getdate, parse_json from erpnext import get_company_currency from erpnext.accounts.doctype.pricing_rule.pricing_rule import ( @@ -889,7 +889,7 @@ def insert_item_price(args): ) -def get_item_price(args, item_code, ignore_party=False): +def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]: """ Get name, price_list_rate from Item Price based on conditions Check if the desired qty is within the increment of the packing list. @@ -906,13 +906,17 @@ def get_item_price(args, item_code, ignore_party=False): (ip.item_code == item_code) & (ip.price_list == args.get("price_list")) & (IfNull(ip.uom, "").isin(["", args.get("uom")])) - & (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) ) .orderby(ip.valid_from, order=frappe.qb.desc) .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc) .orderby(ip.uom, order=frappe.qb.desc) ) + if force_batch_no: + query = query.where(ip.batch_no == args.get("batch_no")) + else: + query = query.where(IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) + if not ignore_party: if args.get("customer"): query = query.where(ip.customer == args.get("customer")) @@ -930,6 +934,21 @@ def get_item_price(args, item_code, ignore_party=False): return query.run() +@frappe.whitelist() +def get_batch_based_item_price(params, item_code) -> float: + if isinstance(params, str): + params = parse_json(params) + + item_price = get_item_price(params, item_code, force_batch_no=True) + if not item_price: + item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True) + + if item_price and item_price[0].uom == params.get("uom"): + return item_price[0].price_list_rate + + return 0.0 + + def get_price_list_rate_for(args, item_code): """ :param customer: link to Customer DocType From ef6b172616ee473c1b05324d2e6217510cec8cda Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:30:36 +0530 Subject: [PATCH 09/40] fix: currency changing while making PO from Supplier Quotation (backport #43187) (#43205) fix: currency changing while making PO from Supplier Quotation (#43187) (cherry picked from commit 2b96e37c3486c5f4e1928cfcb5c3b9f0cad60f6b) Co-authored-by: rohitwaghchaure --- erpnext/public/js/controllers/transaction.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 435ce26c5c1c..67c61118952b 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1258,6 +1258,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "Purchase Receipt": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"], "Purchase Invoice": ["purchase_order_item", "pr_detail", "po_detail"], "Sales Order": ["prevdoc_docname", "quotation_item"], + "Purchase Order": ["supplier_quotation_item"], }; const mappped_fields = mapped_item_field_map[this.frm.doc.doctype] || []; From f101a1ce3b9e2e579ff200c80204a0231916eeec Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 14 Sep 2024 13:36:26 +0530 Subject: [PATCH 10/40] fix(holiday-list): use same date format for same holiday error message (backport #42606) (#43222) fix(holiday-list): use same date format for same holiday error message (#42606) * fix(holiday-list): use same date format for same holiday error message * chore: fix formatting --------- Co-authored-by: Rucha Mahabal (cherry picked from commit a43544153667f8b15d915fd774b46d737de912d2) Co-authored-by: Ananyobrata Pal <74728797+ananyo141@users.noreply.github.com> --- erpnext/setup/doctype/holiday_list/holiday_list.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 0216f75a6286..b7920236ce1c 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -149,7 +149,11 @@ def validate_duplicate_date(self): unique_dates = [] for row in self.holidays: if row.holiday_date in unique_dates: - frappe.throw(_("Holiday Date {0} added multiple times").format(frappe.bold(row.holiday_date))) + frappe.throw( + _("Holiday Date {0} added multiple times").format( + frappe.bold(formatdate(row.holiday_date)) + ) + ) unique_dates.append(row.holiday_date) From f060534625b6a7dcb7b60fd783f041ba3e980650 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 12 Sep 2024 19:38:30 +0530 Subject: [PATCH 11/40] feat: API for crm integration (cherry picked from commit b7bf9f80f2554dc466a108cdcf2e6f929393ac19) --- erpnext/crm/frappe_crm_api.py | 135 ++++++++++++++++++ .../selling/doctype/quotation/quotation.js | 2 +- .../selling/doctype/quotation/quotation.py | 4 + 3 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 erpnext/crm/frappe_crm_api.py diff --git a/erpnext/crm/frappe_crm_api.py b/erpnext/crm/frappe_crm_api.py new file mode 100644 index 000000000000..40c0ce8c9c19 --- /dev/null +++ b/erpnext/crm/frappe_crm_api.py @@ -0,0 +1,135 @@ +import json + +import frappe +from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +@frappe.whitelist() +def create_custom_fields_for_frappe_crm(): + frappe.only_for("System Manager") + custom_fields = { + "Quotation": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "party_name", + } + ], + "Customer": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "prospect_name", + } + ], + } + create_custom_fields(custom_fields, ignore_validate=True) + + +@frappe.whitelist() +def create_prospect_against_crm_deal(): + frappe.only_for("System Manager") + doc = frappe.form_dict + prospect = frappe.get_doc( + { + "doctype": "Prospect", + "company_name": doc.organization or doc.lead_name, + "no_of_employees": doc.no_of_employees, + "prospect_owner": doc.deal_owner, + "company": doc.erpnext_company, + "crm_deal": doc.crm_deal, + "territory": doc.territory, + "industry": doc.industry, + "website": doc.website, + "annual_revenue": doc.annual_revenue, + } + ) + + try: + prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name}) + if not prospect_name: + prospect.insert() + prospect_name = prospect.name + except Exception: + frappe.log_error( + frappe.get_traceback(), + f"Error while creating prospect against CRM Deal: {frappe.form_dict.get('crm_deal_id')}", + ) + pass + + create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name) + frappe.response["message"] = prospect_name + + +def create_contacts(contacts, organization=None, link_doctype=None, link_docname=None): + for c in contacts: + c = frappe._dict(c) + existing_contact = contact_exists(c.email, c.mobile_no) + if existing_contact: + contact = frappe.get_doc("Contact", existing_contact) + else: + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": c.get("full_name"), + "gender": c.get("gender"), + "company_name": organization, + } + ) + + if c.get("email"): + contact.append("email_ids", {"email_id": c.get("email"), "is_primary": 1}) + + if c.get("mobile_no"): + contact.append("phone_nos", {"phone": c.get("mobile_no"), "is_primary_mobile_no": 1}) + + link_contact_to_prospect(contact, link_doctype, link_docname) + + contact.save(ignore_permissions=True) + + +def link_contact_to_prospect(contact, link_doctype, link_docname): + already_linked = any( + [(link.link_doctype == link_doctype and link.link_name == link_docname) for link in contact.links] + ) + if not already_linked: + contact.append( + "links", {"link_doctype": link_doctype, "link_name": link_docname, "link_title": link_docname} + ) + + +def contact_exists(email, mobile_no): + email_exist = frappe.db.exists("Contact Email", {"email_id": email}) + mobile_exist = frappe.db.exists("Contact Phone", {"phone": mobile_no}) + + doctype = "Contact Email" if email_exist else "Contact Phone" + name = email_exist or mobile_exist + + if name: + return frappe.db.get_value(doctype, name, "parent") + + return False + + +@frappe.whitelist() +def create_customer(customer_data=None): + frappe.only_for("System Manager") + if not customer_data: + customer_data = frappe.form_dict + + try: + customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")}) + if not customer_name: + customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert( + ignore_permissions=True + ) + customer_name = customer.name + + contacts = json.loads(customer_data.get("contacts")) + create_contacts(contacts, customer_name, "Customer", customer_name) + except Exception: + frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal") + pass diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 3044d865c0ca..a4c70d7f50f2 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -71,7 +71,7 @@ frappe.ui.form.on("Quotation", { frm.trigger("set_label"); frm.trigger("toggle_reqd_lead_customer"); frm.trigger("set_dynamic_field_label"); - frm.set_value("party_name", ""); + // frm.set_value("party_name", ""); // removed to set party_name from url for crm integration frm.set_value("customer_name", ""); }, diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 2a4b04b9db53..7ebcb3291934 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -220,6 +220,10 @@ def set_customer_name(self): "Lead", self.party_name, ["lead_name", "company_name"] ) self.customer_name = company_name or lead_name + elif self.party_name and self.quotation_to == "Prospect": + self.customer_name = self.party_name + elif self.party_name and self.quotation_to == "CRM Deal": + self.customer_name = frappe.db.get_value("CRM Deal", self.party_name, "organization") def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): From 984acb661d4314960bf2f1eb3542830b182441cc Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 13 Sep 2024 17:30:52 +0530 Subject: [PATCH 12/40] fix: prevent KeyError by checking `report_filter` existence (cherry picked from commit c1d2cc2c14da36508ef1cd72f0ae04c961c1edbf) --- erpnext/patches/v14_0/update_reports_with_range.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/patches/v14_0/update_reports_with_range.py b/erpnext/patches/v14_0/update_reports_with_range.py index 2bda265ca669..014fba883fce 100644 --- a/erpnext/patches/v14_0/update_reports_with_range.py +++ b/erpnext/patches/v14_0/update_reports_with_range.py @@ -30,6 +30,9 @@ def update_report_json(report): report_json = json.loads(report.json) report_filter = report_json.get("filters") + if not report_filter: + return + keys_to_pop = [key for key in report_filter if key.startswith("range")] report_filter["range"] = ", ".join(str(report_filter.pop(key)) for key in keys_to_pop) From d8d4cd23a518ec94b30f69dfc67dd042582fcbec Mon Sep 17 00:00:00 2001 From: venkat102 Date: Sat, 14 Sep 2024 16:00:53 +0530 Subject: [PATCH 13/40] fix(ux): set amount on foreign currency when foreign currency account is selected on last row of journal (cherry picked from commit 2b66842d3492fae20c87eb6969c1c39c33aa8b0e) --- .../doctype/journal_entry/journal_entry.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d290d794df19..999db883e98d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -680,6 +680,7 @@ $.extend(erpnext.journal_entry, { callback: function (r) { if (r.message) { $.extend(d, r.message); + erpnext.journal_entry.set_amount_on_last_row(frm, dt, dn); erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn); refresh_field("accounts"); } @@ -687,4 +688,26 @@ $.extend(erpnext.journal_entry, { }); } }, + set_amount_on_last_row: function (frm, dt, dn) { + let row = locals[dt][dn]; + let length = frm.doc.accounts.length; + if (row.idx != length) return; + + let difference = frm.doc.accounts.reduce((total, row) => { + if (row.idx == length) return total; + + return total + row.debit - row.credit; + }, 0); + + if (difference) { + if (difference > 0) { + row.credit_in_account_currency = difference / row.exchange_rate; + row.credit = difference; + } else { + row.debit_in_account_currency = -difference / row.exchange_rate; + row.debit = -difference; + } + } + refresh_field("accounts"); + }, }); From 2ad6d637ee9193253fbade6ab35769636906c8d1 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 20 Aug 2024 10:58:45 +0530 Subject: [PATCH 14/40] fix: revert 091c5496b20864577d133b0804e957ff8995606f (cherry picked from commit eeb6e75dcf4a79d8ea87c4538f3424348088117d) --- .../doctype/purchase_invoice/purchase_invoice.js | 2 +- .../doctype/purchase_invoice/purchase_invoice.py | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 9c8c8cdfb030..54153b5c19f4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -652,7 +652,7 @@ frappe.ui.form.on("Purchase Invoice", { if (frm.doc.supplier) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } - if (!frm.doc.__onload.enable_apply_tds) { + if (!frm.doc.__onload.supplier_tds) { frm.set_df_property("apply_tds", "read_only", 1); } } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 623fb941b891..c9f3b220cc61 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -346,22 +346,6 @@ def set_missing_values(self, for_validate=False): self.tax_withholding_category = tds_category self.set_onload("supplier_tds", tds_category) - # If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox - if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]: - po = qb.DocType("Purchase Order") - po_with_tds = ( - qb.from_(po) - .select(po.name) - .where( - po.docstatus.eq(1) - & (po.name.isin(purchase_orders)) - & (po.apply_tds.eq(1)) - & (po.tax_withholding_category.notnull()) - ) - .run() - ) - self.set_onload("enable_apply_tds", True if po_with_tds else False) - super().set_missing_values(for_validate) def validate_credit_to_acc(self): From 11359bd2353e7804e226ae000464093d8d5aa71c Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 23 Aug 2024 12:34:07 +0530 Subject: [PATCH 15/40] fix: tds workflow in purchase order (cherry picked from commit a7888b26a73ab61240adf22392bb2ef7b4f93927) --- .../doctype/purchase_order/purchase_order.js | 36 +++++++++++++++++++ .../doctype/purchase_order/purchase_order.py | 7 ++++ 2 files changed, 43 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index bd92ebef3d7e..cab375ea5577 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -65,6 +65,33 @@ frappe.ui.form.on("Purchase Order", { } }, + supplier: function (frm) { + // Do not update if inter company reference is there as the details will already be updated + if (frm.updating_party_details || frm.doc.inter_company_invoice_reference) return; + + if (frm.doc.__onload && frm.doc.__onload.load_after_mapping) return; + + erpnext.utils.get_party_details( + frm, + "erpnext.accounts.party.get_party_details", + { + posting_date: frm.doc.transaction_date, + bill_date: frm.doc.bill_date, + party: frm.doc.supplier, + party_type: "Supplier", + account: frm.doc.credit_to, + price_list: frm.doc.buying_price_list, + fetch_payment_terms_template: cint(!frm.doc.ignore_default_payment_terms_template), + }, + function () { + frm.doc.apply_tds = frm.supplier_tds ? 1 : 0; + frm.doc.tax_withholding_category = frm.supplier_tds; + frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1); + frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1); + } + ); + }, + get_materials_from_supplier: function (frm) { let po_details = []; @@ -108,6 +135,15 @@ frappe.ui.form.on("Purchase Order", { frm.set_value("transaction_date", frappe.datetime.get_today()); } + if (frm.doc.__onload && frm.is_new()) { + if (frm.doc.supplier) { + frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; + } + if (!frm.doc.__onload.supplier_tds) { + frm.set_df_property("apply_tds", "read_only", 1); + } + } + erpnext.queries.setup_queries(frm, "Warehouse", function () { return erpnext.queries.warehouse(frm.doc); }); diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 0508483a0fc9..32968bc6a567 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -648,6 +648,13 @@ def update_subcontracting_order_status(self): if sco: update_sco_status(sco, "Closed" if self.status == "Closed" else None) + def set_missing_values(self, for_validate=False): + tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") + if tds_category and not for_validate: + self.apply_tds = 1 + self.tax_withholding_category = tds_category + self.set_onload("supplier_tds", tds_category) + @frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): From 5edebb28a5bcbeacc1084c9da3cb99e156d95b8d Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 3 Sep 2024 11:44:55 +0530 Subject: [PATCH 16/40] fix: do not check appy_tds in Purchase Order Automatically (cherry picked from commit be6c174b439c6a3729f89583c74723921e1fb9f5) --- erpnext/buying/doctype/purchase_order/purchase_order.js | 2 -- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index cab375ea5577..a40ef47afcf0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -84,8 +84,6 @@ frappe.ui.form.on("Purchase Order", { fetch_payment_terms_template: cint(!frm.doc.ignore_default_payment_terms_template), }, function () { - frm.doc.apply_tds = frm.supplier_tds ? 1 : 0; - frm.doc.tax_withholding_category = frm.supplier_tds; frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1); frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1); } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 32968bc6a567..6c311953aead 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -655,6 +655,8 @@ def set_missing_values(self, for_validate=False): self.tax_withholding_category = tds_category self.set_onload("supplier_tds", tds_category) + super().set_missing_values(for_validate) + @frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): From 01f30682ee9e15a51161a4f2a91681484e6ac8bf Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 3 Sep 2024 13:58:30 +0530 Subject: [PATCH 17/40] fix: consistent behaviour on refresh (cherry picked from commit b216d7127816a9b61c3ca20c645e5b78e9984be6) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 4 ++-- erpnext/buying/doctype/purchase_order/purchase_order.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 54153b5c19f4..2d5cbb9e6c3a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -648,8 +648,8 @@ frappe.ui.form.on("Purchase Invoice", { }, onload: function (frm) { - if (frm.doc.__onload && frm.is_new()) { - if (frm.doc.supplier) { + if (frm.doc.__onload && frm.doc.supplier) { + if (frm.is_new()) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } if (!frm.doc.__onload.supplier_tds) { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index a40ef47afcf0..ac2aa43f23db 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -133,8 +133,8 @@ frappe.ui.form.on("Purchase Order", { frm.set_value("transaction_date", frappe.datetime.get_today()); } - if (frm.doc.__onload && frm.is_new()) { - if (frm.doc.supplier) { + if (frm.doc.__onload && frm.doc.supplier) { + if (frm.is_new()) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } if (!frm.doc.__onload.supplier_tds) { From 7027be8fbcf51282ea470299e4edb3d9ce89dd6f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 4 Sep 2024 13:57:03 +0530 Subject: [PATCH 18/40] fix: set tax_withholding_category from Purchase Order while creating pi form po (cherry picked from commit b9048ca6faad26837626f42a9cc475f96d4a4894) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 6c311953aead..afb43e30c7b0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -769,6 +769,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) + + # set tax_withholding_category from Purchase Order + if source.apply_tds and source.tax_withholding_category and target.apply_tds: + target.tax_withholding_category = source.tax_withholding_category + # Get the advance paid Journal Entries in Purchase Invoice Advance if target.get("allocate_advances_automatically"): target.set_advances() From 741c18b14468f29586cc9c19c293b6f6518249e1 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 10 Sep 2024 13:11:09 +0530 Subject: [PATCH 19/40] fix: do not auto apply tds in purchase order (cherry picked from commit 0b942a06144977a052b48d6b61daf9c61816c82d) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index afb43e30c7b0..14424dfdf4ab 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -651,8 +651,6 @@ def update_subcontracting_order_status(self): def set_missing_values(self, for_validate=False): tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") if tds_category and not for_validate: - self.apply_tds = 1 - self.tax_withholding_category = tds_category self.set_onload("supplier_tds", tds_category) super().set_missing_values(for_validate) From 42494db3c77db1ec590bf7e73bb89828cd06b029 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 11 Sep 2024 18:54:55 +0530 Subject: [PATCH 20/40] fix: hide and reset discount control on new POS order (cherry picked from commit 5b0053f8ddbb93e2f5e6921b72ea6cbccb265f5c) --- .../page/point_of_sale/pos_item_cart.js | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 694f70d4db5e..b808b4f8828d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -389,28 +389,14 @@ erpnext.PointOfSale.ItemCart = class { placeholder: discount ? discount + "%" : __("Enter discount percentage."), input_class: "input-xs", onchange: function () { - if (flt(this.value) != 0) { - frappe.model.set_value( - frm.doc.doctype, - frm.doc.name, - "additional_discount_percentage", - flt(this.value) - ); - me.hide_discount_control(this.value); - } else { - frappe.model.set_value( - frm.doc.doctype, - frm.doc.name, - "additional_discount_percentage", - 0 - ); - me.$add_discount_elem.css({ - border: "1px dashed var(--gray-500)", - padding: "var(--padding-sm) var(--padding-md)", - }); - me.$add_discount_elem.html(`${me.get_discount_icon()} ${__("Add Discount")}`); - me.discount_field = undefined; - } + this.value = flt(this.value); + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "additional_discount_percentage", + flt(this.value) + ); + me.hide_discount_control(this.value); }, }, parent: this.$add_discount_elem.find(".add-discount-field"), @@ -421,9 +407,13 @@ erpnext.PointOfSale.ItemCart = class { } hide_discount_control(discount) { - if (!discount) { - this.$add_discount_elem.css({ padding: "0px", border: "none" }); - this.$add_discount_elem.html(`
`); + if (!flt(discount)) { + this.$add_discount_elem.css({ + border: "1px dashed var(--gray-500)", + padding: "var(--padding-sm) var(--padding-md)", + }); + this.$add_discount_elem.html(`${this.get_discount_icon()} ${__("Add Discount")}`); + this.discount_field = undefined; } else { this.$add_discount_elem.css({ border: "1px dashed var(--dark-green-500)", @@ -1051,6 +1041,7 @@ erpnext.PointOfSale.ItemCart = class { this.highlight_checkout_btn(false); } + this.hide_discount_control(frm.doc.additional_discount_percentage); this.update_totals_section(frm); if (frm.doc.docstatus === 1) { From d6a3d0d46835559dffb50c502bfec10d15492031 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 16 Sep 2024 18:47:34 +0530 Subject: [PATCH 21/40] fix: create and link address while creating prospect & customer (cherry picked from commit 035c15794cd86407fffd2dcc89cc436ba3a3dd6a) --- erpnext/crm/frappe_crm_api.py | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/frappe_crm_api.py b/erpnext/crm/frappe_crm_api.py index 40c0ce8c9c19..53f58ed57eb1 100644 --- a/erpnext/crm/frappe_crm_api.py +++ b/erpnext/crm/frappe_crm_api.py @@ -61,6 +61,7 @@ def create_prospect_against_crm_deal(): pass create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name) + create_address("Prospect", prospect_name, doc.address) frappe.response["message"] = prospect_name @@ -86,17 +87,49 @@ def create_contacts(contacts, organization=None, link_doctype=None, link_docname if c.get("mobile_no"): contact.append("phone_nos", {"phone": c.get("mobile_no"), "is_primary_mobile_no": 1}) - link_contact_to_prospect(contact, link_doctype, link_docname) + link_doc(contact, link_doctype, link_docname) contact.save(ignore_permissions=True) -def link_contact_to_prospect(contact, link_doctype, link_docname): +def create_address(doctype, docname, address): + if not address: + return + try: + _address = frappe.db.exists("Address", address.get("name")) + if not _address: + new_address_doc = frappe.new_doc("Address") + for field in [ + "address_title", + "address_type", + "address_line1", + "address_line2", + "city", + "state", + "pincode", + "country", + ]: + if address.get(field): + new_address_doc.set(field, address.get(field)) + + new_address_doc.append("links", {"link_doctype": doctype, "link_name": docname}) + new_address_doc.insert(ignore_mandatory=True) + return new_address_doc.name + else: + address = frappe.get_doc("Address", _address) + link_doc(address, doctype, docname) + address.save(ignore_permissions=True) + return address.name + except Exception: + frappe.log_error(frappe.get_traceback(), f"Error while creating address for {docname}") + + +def link_doc(doc, link_doctype, link_docname): already_linked = any( - [(link.link_doctype == link_doctype and link.link_name == link_docname) for link in contact.links] + [(link.link_doctype == link_doctype and link.link_name == link_docname) for link in doc.links] ) if not already_linked: - contact.append( + doc.append( "links", {"link_doctype": link_doctype, "link_name": link_docname, "link_title": link_docname} ) @@ -130,6 +163,8 @@ def create_customer(customer_data=None): contacts = json.loads(customer_data.get("contacts")) create_contacts(contacts, customer_name, "Customer", customer_name) + create_address("Customer", customer_name, customer_data.get("address")) + return customer_name except Exception: frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal") pass From c5051561e4eb14374e85245c1068e807ef8b15a0 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:35:35 +0530 Subject: [PATCH 22/40] fix: do not validate purchase document for composite asset (cherry picked from commit 5fd058dde9c5afe1e8b3148ac22bcf5f28aa5023) --- erpnext/assets/doctype/asset/asset.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 58f813247f0a..6c77a1b9b0ee 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -623,6 +623,9 @@ def get_manual_depreciation_entries(self): return records def validate_make_gl_entry(self): + if self.is_composite_asset: + return True + purchase_document = self.get_purchase_document() if not purchase_document: return False From f7cedac5260d832aa6b95b9f0341c94d7ccd6203 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Sat, 14 Sep 2024 17:39:50 +0530 Subject: [PATCH 23/40] fix(ux): set amount based on account currency while adding new row (cherry picked from commit 0ff04f774d5e54ce83e239896e7ab51d083021fe) --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 999db883e98d..faa38763b80d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro accounts_add(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); + row.exchange_rate = 1; $.each(doc.accounts, function (i, d) { if (d.account && d.party && d.party_type) { row.account = d.account; row.party = d.party; row.party_type = d.party_type; + row.exchange_rate = d.exchange_rate; } }); // set difference if (doc.difference) { if (doc.difference > 0) { - row.credit_in_account_currency = doc.difference; + row.credit_in_account_currency = doc.difference / row.exchange_rate; row.credit = doc.difference; } else { - row.debit_in_account_currency = -doc.difference; + row.debit_in_account_currency = -doc.difference / row.exchange_rate; row.debit = -doc.difference; } } From dea735de4df2ca427fce8803d3d4f8acdc89c789 Mon Sep 17 00:00:00 2001 From: "lukas.brandhoff" Date: Wed, 28 Aug 2024 09:17:28 +0000 Subject: [PATCH 24/40] fix: A project without tasks should be able to complete (cherry picked from commit 268962c25f0571ded4c46c381906c870df6aab91) --- erpnext/projects/doctype/project/project.py | 7 +++++ .../projects/doctype/project/test_project.py | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index b09735e56443..7870a2ace738 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -213,6 +213,13 @@ def on_trash(self): frappe.db.set_value("Sales Order", {"project": self.name}, "project", "") def update_percent_complete(self): + if self.status == "Completed": + if ( + len(frappe.get_all("Task", dict(project=self.name))) == 0 + ): # A project without tasks should be able to complete + self.percent_complete_method = "Manual" + self.percent_complete = 100 + if self.percent_complete_method == "Manual": if self.status == "Completed": self.percent_complete = 100 diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 1b7460f7a2a0..f7140c211e04 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -199,6 +199,34 @@ def test_project_with_template_tasks_having_common_name(self): if not pt.is_group: self.assertIsNotNone(pt.parent_task) + def test_project_having_no_tasks_complete(self): + project_name = "Test Project - No Tasks Completion" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc("Project", project_name) + + project = frappe.get_doc( + dict( + doctype="Project", + project_name=project_name, + status="Open", + expected_start_date=nowdate(), + company="_Test Company", + ) + ).insert() + + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"], + dict(project=project.name), + order_by="creation asc", + ) + + self.assertEqual(project.status, "Open") + self.assertEqual(len(tasks), 0) + project.status = "Completed" + project.save() + self.assertEqual(project.status, "Completed") + def get_project(name, template): project = frappe.get_doc( From c9b6b0d8683ef2328ba7892b295c3ca05256cd94 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 16 Sep 2024 11:14:54 +0530 Subject: [PATCH 25/40] refactor(test): fix linter (cherry picked from commit 4eeae8011ea51dc5621cef302255c2b54da1537a) --- erpnext/projects/doctype/project/test_project.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index f7140c211e04..e5996c2da9d2 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -205,13 +205,13 @@ def test_project_having_no_tasks_complete(self): frappe.delete_doc("Project", project_name) project = frappe.get_doc( - dict( - doctype="Project", - project_name=project_name, - status="Open", - expected_start_date=nowdate(), - company="_Test Company", - ) + { + "doctype": "Project", + "project_name": project_name, + "status": "Open", + "expected_start_date": nowdate(), + "company": "_Test Company", + } ).insert() tasks = frappe.get_all( From 291f0a580be176a4c87c03e54372dd7a7cca9cb1 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 12 Sep 2024 09:18:19 +0000 Subject: [PATCH 26/40] fix: invalid gp calculation (cherry picked from commit c79851239cf3fa663dff2e10a5d5738e7bfa7abf) --- erpnext/controllers/selling_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 766e67be0f67..7ad12b54eff5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -640,7 +640,7 @@ def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), + ((flt(item.stock_uom_rate) - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item), ) From 86e1818420a3152cf47ef65943216c6a9bd0d29e Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Sat, 14 Sep 2024 15:58:02 +0530 Subject: [PATCH 27/40] fix: map rows on journal entry by validating account, party, debit and credit value (cherry picked from commit b634aa9cfb60f78531da4a02efec458a3d294a78) --- erpnext/controllers/accounts_controller.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 92efa5168f3b..b35be7950fde 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3535,6 +3535,13 @@ def check_if_child_table_updated(child_table_before_update, child_table_after_up # Check if any field affecting accounting entry is altered for index, item in enumerate(child_table_before_update): + if item.parenttype == "Journal Entry" and any( + [ + child_table_after_update[index].get(i) != item.get(i) + for i in ["account", "party_type", "party", "debit", "credit"] + ] + ): + continue for field in fields_to_check: if child_table_after_update[index].get(field) != item.get(field): return True From 310b1314698710c1f82c820cdf6a78aaaebe5fdc Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Sat, 14 Sep 2024 17:31:53 +0530 Subject: [PATCH 28/40] test: reconcile payment jv from closed fiscal year (cherry picked from commit f47ea468066c25db8d6765205733e549c6dce190) --- .../test_payment_reconciliation.py | 162 +++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 5aa411158a8f..65d7d42ef3da 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -5,7 +5,7 @@ import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -13,6 +13,7 @@ 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.accounts.party import get_party_account +from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.stock.doctype.item.test_item import create_item @@ -1845,6 +1846,107 @@ def test_cr_note_payment_limit_filter(self): self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) + def test_reconciliation_on_closed_period_payment(self): + from erpnext.accounts.doctype.account.test_account import create_account + + self.create_company() + self.create_cost_center() + + # create bank account + parent_account = frappe.db.get_value( + "Account", {"company": self.company, "account_name": "Bank Accounts", "is_group": 1}, "name" + ) + bank_account = create_account( + account_name="Bank Account", + account_type="Bank", + is_group=0, + company=self.company, + root_type="Asset", + report_type="Balance Sheet", + account_currency="INR", + parent_account=parent_account, + doctype="Account", + ) + + # create backdated fiscal year + create_fiscal_year(company=self.company, year_start_date="1990-04-01", year_end_date="1991-03-31") + + # make journal entry for previous year + je_1 = frappe.new_doc("Journal Entry") + je_1.posting_date = "1990-06-01" + je_1.company = self.company + je_1.user_remark = "test" + je_1.set( + "accounts", + [ + { + "account": self.debit_to, + "cost_center": self.cost_center, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 0, + "credit_in_account_currency": 1000, + }, + { + "account": bank_account, + "cost_center": self.sub_cc, + "credit_in_account_currency": 0, + "debit_in_account_currency": 500, + }, + { + "account": "Cash - _PR", + "cost_center": self.sub_cc, + "credit_in_account_currency": 0, + "debit_in_account_currency": 500, + }, + ], + ) + je_1.save() + je_1.submit() + je_1.reload() + # check journal entry is submitted + self.assertTrue(je_1.docstatus == 1) + + # make period closing voucher + pcv = make_period_closing_voucher( + company=self.company, cost_center=self.cost_center, posting_date="1991-03-31" + ) + pcv.reload() + # check if period closing voucher is completed + self.assertEqual(pcv.gle_processing_status, "Completed") + + # make journal entry for active year + je_2 = self.create_journal_entry( + acc1=self.debit_to, acc2=self.income_account, amount=1000, posting_date=today() + ) + je_2.accounts[0].party_type = "Customer" + je_2.accounts[0].party = self.customer + je_2.save() + je_2.submit() + je_2.reload() + # check journal entry is submitted + self.assertTrue(je_2.docstatus == 1) + + # process reconciliation on closed period payment + pr = self.create_payment_reconciliation(party_is_customer=True) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = None + pr.get_unreconciled_entries() + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + je_1.reload() + je_2.reload() + + # check whether the payment reconciliation is done on the closed period + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + + # cancel created entires during test + pcv.cancel() + je_1.cancel() + je_2.cancel() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): @@ -1872,3 +1974,61 @@ def make_supplier(supplier_name, currency=None): return supplier.name else: return supplier_name + + +def create_fiscal_year(company, year_start_date, year_end_date): + fy_docname = frappe.db.exists( + "Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date} + ) + if not fy_docname: + fy_doc = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": f"{getdate(year_start_date).year}-{getdate(year_end_date).year}", + "year_start_date": year_start_date, + "year_end_date": year_end_date, + "companies": [{"company": company}], + } + ).save() + return fy_doc + else: + fy_doc = frappe.get_doc("Fiscal Year", fy_docname) + if not frappe.db.exists("Fiscal Year Company", {"parent": fy_docname, "company": company}): + fy_doc.append("companies", {"company": company}) + fy_doc.save() + return fy_doc + + +def make_period_closing_voucher(company, cost_center, posting_date=None, submit=True): + from erpnext.accounts.doctype.account.test_account import create_account + + parent_account = frappe.db.get_value( + "Account", {"company": company, "account_name": "Current Liabilities", "is_group": 1}, "name" + ) + surplus_account = create_account( + account_name="Reserve and Surplus", + is_group=0, + company=company, + root_type="Liability", + report_type="Balance Sheet", + account_currency="INR", + parent_account=parent_account, + doctype="Account", + ) + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": posting_date or today(), + "posting_date": posting_date or today(), + "company": company, + "fiscal_year": get_fiscal_year(today(), company=company)[0], + "cost_center": cost_center, + "closing_account_head": surplus_account, + "remarks": "test", + } + ) + pcv.insert() + if submit: + pcv.submit() + + return pcv From d91013a46707095f62a009a2fbdcf84bd1303c62 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Sep 2024 14:38:26 +0530 Subject: [PATCH 29/40] fix: ignore repost logic on Payment Reconciliation (cherry picked from commit 75babd4c18fc555046750a2b640d0ac1f4f435bd) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 5 +++++ erpnext/accounts/utils.py | 2 ++ erpnext/controllers/accounts_controller.py | 7 ------- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 6adc8be3f7db..593fa48e8560 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -195,6 +195,11 @@ def on_submit(self): self.update_booked_depreciation() def on_update_after_submit(self): + # Flag will be set on Reconciliation + # Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost. + if self.flags.get("ignore_reposting_on_reconciliation"): + return + self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []}) if self.needs_repost: self.validate_for_repost() diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 32eeb1e07a66..8e47ddc3652b 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -665,6 +665,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): # will work as update after submit journal_entry.flags.ignore_validate_update_after_submit = True + # Ledgers will be reposted by Reconciliation tool + journal_entry.flags.ignore_reposting_on_reconciliation = True if not do_not_save: journal_entry.save(ignore_permissions=True) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b35be7950fde..92efa5168f3b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3535,13 +3535,6 @@ def check_if_child_table_updated(child_table_before_update, child_table_after_up # Check if any field affecting accounting entry is altered for index, item in enumerate(child_table_before_update): - if item.parenttype == "Journal Entry" and any( - [ - child_table_after_update[index].get(i) != item.get(i) - for i in ["account", "party_type", "party", "debit", "credit"] - ] - ): - continue for field in fields_to_check: if child_table_after_update[index].get(field) != item.get(field): return True From 861edb438bb11e246c2eb9f8e1953a84bcdb0461 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Sep 2024 16:49:03 +0530 Subject: [PATCH 30/40] refactor(test): make use existing test data and dynamic fy creation (cherry picked from commit f45638015f8bce4efe7cea3f0035c060d42a3426) --- .../test_payment_reconciliation.py | 51 +++++-------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 65d7d42ef3da..b229c9c2c40d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1849,31 +1849,19 @@ def test_cr_note_payment_limit_filter(self): def test_reconciliation_on_closed_period_payment(self): from erpnext.accounts.doctype.account.test_account import create_account - self.create_company() - self.create_cost_center() - - # create bank account - parent_account = frappe.db.get_value( - "Account", {"company": self.company, "account_name": "Bank Accounts", "is_group": 1}, "name" - ) - bank_account = create_account( - account_name="Bank Account", - account_type="Bank", - is_group=0, - company=self.company, - root_type="Asset", - report_type="Balance Sheet", - account_currency="INR", - parent_account=parent_account, - doctype="Account", - ) + # Get current fiscal year + current_fy_start_date = get_fiscal_year(today())[1] # create backdated fiscal year - create_fiscal_year(company=self.company, year_start_date="1990-04-01", year_end_date="1991-03-31") + prev_fy_start_date = add_days(current_fy_start_date, -366) + prev_fy_end_date = add_days(current_fy_start_date, -1) + create_fiscal_year( + company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date + ) # make journal entry for previous year je_1 = frappe.new_doc("Journal Entry") - je_1.posting_date = "1990-06-01" + je_1.posting_date = add_days(prev_fy_start_date, 20) je_1.company = self.company je_1.user_remark = "test" je_1.set( @@ -1888,28 +1876,24 @@ def test_reconciliation_on_closed_period_payment(self): "credit_in_account_currency": 1000, }, { - "account": bank_account, - "cost_center": self.sub_cc, + "account": self.bank, + "cost_center": self.sub_cc.name, "credit_in_account_currency": 0, "debit_in_account_currency": 500, }, { - "account": "Cash - _PR", - "cost_center": self.sub_cc, + "account": self.cash, + "cost_center": self.sub_cc.name, "credit_in_account_currency": 0, "debit_in_account_currency": 500, }, ], ) - je_1.save() je_1.submit() - je_1.reload() - # check journal entry is submitted - self.assertTrue(je_1.docstatus == 1) # make period closing voucher pcv = make_period_closing_voucher( - company=self.company, cost_center=self.cost_center, posting_date="1991-03-31" + company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date ) pcv.reload() # check if period closing voucher is completed @@ -1921,11 +1905,7 @@ def test_reconciliation_on_closed_period_payment(self): ) je_2.accounts[0].party_type = "Customer" je_2.accounts[0].party = self.customer - je_2.save() je_2.submit() - je_2.reload() - # check journal entry is submitted - self.assertTrue(je_2.docstatus == 1) # process reconciliation on closed period payment pr = self.create_payment_reconciliation(party_is_customer=True) @@ -1942,11 +1922,6 @@ def test_reconciliation_on_closed_period_payment(self): self.assertEqual(pr.get("invoices"), []) self.assertEqual(pr.get("payments"), []) - # cancel created entires during test - pcv.cancel() - je_1.cancel() - je_2.cancel() - def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): From edcdfdd194be8d84b5009c18f725868a7bb572d9 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Tue, 17 Sep 2024 17:21:26 +0530 Subject: [PATCH 31/40] refactor: update formatting changes (cherry picked from commit 768bb0312aae3ad67191778f09fe75f4c914a019) --- .../payment_reconciliation/test_payment_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index b229c9c2c40d..f317f3399db0 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1996,7 +1996,7 @@ def make_period_closing_voucher(company, cost_center, posting_date=None, submit= "transaction_date": posting_date or today(), "posting_date": posting_date or today(), "company": company, - "fiscal_year": get_fiscal_year(today(), company=company)[0], + "fiscal_year": get_fiscal_year(posting_date or today(), company=company)[0], "cost_center": cost_center, "closing_account_head": surplus_account, "remarks": "test", From 78768f883c09ec91de2dfee101923808ccd6fa55 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Tue, 17 Sep 2024 18:55:28 +0530 Subject: [PATCH 32/40] fix: create fiscal year without overlapping existing Fiscal Years (cherry picked from commit 720a330617c7cb5410bee715dbb4a4111af879f7) --- .../test_payment_reconciliation.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index f317f3399db0..883c638398c5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -5,7 +5,7 @@ import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, getdate, nowdate, today +from frappe.utils import add_days, add_years, flt, getdate, nowdate, today from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -1847,14 +1847,10 @@ def test_cr_note_payment_limit_filter(self): self.assertEqual(len(pr.payments), 1) def test_reconciliation_on_closed_period_payment(self): - from erpnext.accounts.doctype.account.test_account import create_account - - # Get current fiscal year - current_fy_start_date = get_fiscal_year(today())[1] - # create backdated fiscal year - prev_fy_start_date = add_days(current_fy_start_date, -366) - prev_fy_end_date = add_days(current_fy_start_date, -1) + first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)") + prev_fy_start_date = add_years(first_fy_start_date, -1) + prev_fy_end_date = add_days(first_fy_start_date, -1) create_fiscal_year( company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date ) From 78c68397d9a5147970f4b19435c5352ced2a6a44 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:26:21 +0530 Subject: [PATCH 33/40] fix: updated filtering in depreciation and balances report (cherry picked from commit 3a34eecdcfe394d336cd5abd1734ff701eac32b5) --- .../asset_depreciations_and_balances.py | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 34cced2ca171..caddaf91f825 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -232,9 +232,14 @@ def get_group_by_asset_data(filters): def get_assets_for_grouped_by_category(filters): condition = "" if filters.get("asset_category"): - condition = " and a.asset_category = '{}'".format(filters.get("asset_category")) + condition = f" and a.asset_category = '{filters.get('asset_category')}'" + finance_book_filter = "" + if filters.get("finance_book"): + finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + return frappe.db.sql( - """ + f""" SELECT results.asset_category, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -264,7 +269,14 @@ def get_assets_for_grouped_by_category(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} + where + a.docstatus=1 + and a.company=%(company)s + and a.purchase_date <= %(to_date)s + and gle.debit != 0 + and gle.is_cancelled = 0 + and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + {condition} {finance_book_filter} group by a.asset_category union SELECT a.asset_category, @@ -280,11 +292,16 @@ def get_assets_for_grouped_by_category(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} group by a.asset_category) as results group by results.asset_category - """.format(condition), - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """, + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "finance_book": filters.get("finance_book", ""), + }, as_dict=1, ) @@ -292,9 +309,14 @@ def get_assets_for_grouped_by_category(filters): def get_assets_for_grouped_by_asset(filters): condition = "" if filters.get("asset"): - condition = " and a.name = '{}'".format(filters.get("asset")) + condition = f" and a.name = '{filters.get('asset')}'" + finance_book_filter = "" + if filters.get("finance_book"): + finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + return frappe.db.sql( - """ + f""" SELECT results.name as asset, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -324,7 +346,14 @@ def get_assets_for_grouped_by_asset(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} + where + a.docstatus=1 + and a.company=%(company)s + and a.purchase_date <= %(to_date)s + and gle.debit != 0 + and gle.is_cancelled = 0 + and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + {finance_book_filter} {condition} group by a.name union SELECT a.name as name, @@ -340,11 +369,16 @@ def get_assets_for_grouped_by_asset(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} group by a.name) as results group by results.name - """.format(condition), - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """, + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "finance_book": filters.get("finance_book", ""), + }, as_dict=1, ) From 1e89c007ed8d6e38f8b3b9d8bcc2c1690599b25b Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:45:29 +0530 Subject: [PATCH 34/40] chore: resolved linter check with #nosemgrep (cherry picked from commit 8c8e25214c3042346d8e31514b27f48089ce2948) --- .../asset_depreciations_and_balances.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index caddaf91f825..2d80808a7e6e 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -238,6 +238,7 @@ def get_assets_for_grouped_by_category(filters): finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + # nosemgrep return frappe.db.sql( f""" SELECT results.asset_category, @@ -306,6 +307,7 @@ def get_assets_for_grouped_by_category(filters): ) +# nosemgrep def get_assets_for_grouped_by_asset(filters): condition = "" if filters.get("asset"): From 4ba37e49d8eee249b9f33255c82a5390625af158 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:49:01 +0530 Subject: [PATCH 35/40] chore: resolved failing check (cherry picked from commit af52f0e71fa992f89b55795eb14f12e125e56720) --- .../asset_depreciations_and_balances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 2d80808a7e6e..b288e8e5ac73 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -307,7 +307,6 @@ def get_assets_for_grouped_by_category(filters): ) -# nosemgrep def get_assets_for_grouped_by_asset(filters): condition = "" if filters.get("asset"): @@ -317,6 +316,7 @@ def get_assets_for_grouped_by_asset(filters): finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + # nosemgrep return frappe.db.sql( f""" SELECT results.name as asset, From 0fe901a137c1b4d3ab5caee046ad4cc8911d7444 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Fri, 13 Sep 2024 16:48:32 +0530 Subject: [PATCH 36/40] fix: fetch cost center allocation percentage only from the applicable allocation (cherry picked from commit 36e5945c667595811688bd9eeaa07af9ecc5ab08) --- erpnext/accounts/general_ledger.py | 63 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index a4d128a58459..ad8cc97e101d 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): - cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"]) - if not cost_center_allocation: - return gl_map - new_gl_map = [] for d in gl_map: cost_center = d.get("cost_center") # Validate budget against main cost center validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)) - - if cost_center and cost_center_allocation.get(cost_center): - for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items(): - gle = copy.deepcopy(d) - gle.cost_center = sub_cost_center - for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"): - gle[field] = flt(flt(d.get(field)) * percentage / 100, precision) - new_gl_map.append(gle) - else: + cost_center_allocation = get_cost_center_allocation_data( + gl_map[0]["company"], gl_map[0]["posting_date"], cost_center + ) + if not cost_center_allocation: new_gl_map.append(d) + continue + + for sub_cost_center, percentage in cost_center_allocation: + gle = copy.deepcopy(d) + gle.cost_center = sub_cost_center + for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"): + gle[field] = flt(flt(d.get(field)) * percentage / 100, precision) + new_gl_map.append(gle) return new_gl_map -def get_cost_center_allocation_data(company, posting_date): - par = frappe.qb.DocType("Cost Center Allocation") - child = frappe.qb.DocType("Cost Center Allocation Percentage") +def get_cost_center_allocation_data(company, posting_date, cost_center): + cost_center_allocation = frappe.db.get_value( + "Cost Center Allocation", + { + "docstatus": 1, + "company": company, + "valid_from": ("<=", posting_date), + "main_cost_center": cost_center, + }, + pluck="name", + order_by="valid_from desc", + ) - records = ( - frappe.qb.from_(par) - .inner_join(child) - .on(par.name == child.parent) - .select(par.main_cost_center, child.cost_center, child.percentage) - .where(par.docstatus == 1) - .where(par.company == company) - .where(par.valid_from <= posting_date) - .orderby(par.valid_from, order=frappe.qb.desc) - ).run(as_dict=True) + if not cost_center_allocation: + return [] - cc_allocation = frappe._dict() - for d in records: - cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage) + records = frappe.db.get_all( + "Cost Center Allocation Percentage", + {"parent": cost_center_allocation}, + ["cost_center", "percentage"], + as_list=True, + ) - return cc_allocation + return records def merge_similar_entries(gl_map, precision=None): From 52a161f076b26cbf46811fbf14eff18072657422 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Fri, 13 Sep 2024 16:54:29 +0530 Subject: [PATCH 37/40] test: add unit test for validating multiple cost center allocation with different child cost center (cherry picked from commit 4d5d6150e134a3af99cbd4a9b5e3eb61c87dd636) --- .../test_cost_center_allocation.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py index 65784dbb6c72..c333bbbf1305 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py @@ -24,6 +24,7 @@ def setUp(self): "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2", + "Sub Cost Center 3", ] for cc in cost_centers: create_cost_center(cost_center_name=cc, company="_Test Company") @@ -141,6 +142,49 @@ def test_valid_from_based_on_existing_gle(self): jv.cancel() + def test_multiple_cost_center_allocation_on_same_main_cost_center(self): + create_cost_center_allocation( + "_Test Company", + "Main Cost Center 1 - _TC", + {"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40}, + valid_from=add_days(today(), -5), + ) + + create_cost_center_allocation( + "_Test Company", + "Main Cost Center 1 - _TC", + {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}, + valid_from=add_days(today(), -1), + ) + + jv = make_journal_entry( + "Cash - _TC", + "Sales - _TC", + 100, + cost_center="Main Cost Center 1 - _TC", + posting_date=today(), + submit=True, + ) + + expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50} + + gle = frappe.qb.DocType("GL Entry") + gl_entries = ( + frappe.qb.from_(gle) + .select(gle.cost_center, gle.debit, gle.credit) + .where(gle.voucher_type == "Journal Entry") + .where(gle.voucher_no == jv.name) + .where(gle.account == "Sales - _TC") + .orderby(gle.cost_center) + ).run(as_dict=1) + + self.assertTrue(gl_entries) + + for gle in gl_entries: + self.assertTrue(gle.cost_center in expected_values) + self.assertEqual(gle.debit, 0) + self.assertEqual(gle.credit, expected_values[gle.cost_center]) + def create_cost_center_allocation( company, From 3d29007aeb053fad0b8040fa1753305def7a4948 Mon Sep 17 00:00:00 2001 From: venkat102 Date: Fri, 13 Sep 2024 18:46:10 +0530 Subject: [PATCH 38/40] fix: cancel cost center allocation and journal entry after test (cherry picked from commit 3c65b98b49d2e7760c4bc01fcc0221f77a529a2b) --- .../test_cost_center_allocation.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py index c333bbbf1305..4abc82d8becd 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py @@ -22,6 +22,7 @@ def setUp(self): cost_centers = [ "Main Cost Center 1", "Main Cost Center 2", + "Main Cost Center 3", "Sub Cost Center 1", "Sub Cost Center 2", "Sub Cost Center 3", @@ -37,7 +38,7 @@ def test_gle_based_on_cost_center_allocation(self): ) jv = make_journal_entry( - "_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True + "Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True ) expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]] @@ -121,7 +122,7 @@ def test_total_percentage(self): def test_valid_from_based_on_existing_gle(self): # GLE posted against Sub Cost Center 1 on today jv = make_journal_entry( - "_Test Cash - _TC", + "Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", @@ -143,16 +144,16 @@ def test_valid_from_based_on_existing_gle(self): jv.cancel() def test_multiple_cost_center_allocation_on_same_main_cost_center(self): - create_cost_center_allocation( + coa1 = create_cost_center_allocation( "_Test Company", - "Main Cost Center 1 - _TC", + "Main Cost Center 3 - _TC", {"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40}, valid_from=add_days(today(), -5), ) - create_cost_center_allocation( + coa2 = create_cost_center_allocation( "_Test Company", - "Main Cost Center 1 - _TC", + "Main Cost Center 3 - _TC", {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}, valid_from=add_days(today(), -1), ) @@ -161,7 +162,7 @@ def test_multiple_cost_center_allocation_on_same_main_cost_center(self): "Cash - _TC", "Sales - _TC", 100, - cost_center="Main Cost Center 1 - _TC", + cost_center="Main Cost Center 3 - _TC", posting_date=today(), submit=True, ) @@ -185,6 +186,10 @@ def test_multiple_cost_center_allocation_on_same_main_cost_center(self): self.assertEqual(gle.debit, 0) self.assertEqual(gle.credit, expected_values[gle.cost_center]) + coa1.cancel() + coa2.cancel() + jv.cancel() + def create_cost_center_allocation( company, From 782c9dda1a7af57d330294d3b5ef51fd3e3904ef Mon Sep 17 00:00:00 2001 From: krishna Date: Mon, 16 Sep 2024 18:44:58 +0530 Subject: [PATCH 39/40] fix: add currency in options for rate field in pricing rule (cherry picked from commit 636c0131fa7f43a2902e18e68663418e2bf625f5) # Conflicts: # erpnext/accounts/doctype/pricing_rule/pricing_rule.json --- erpnext/accounts/doctype/pricing_rule/pricing_rule.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 6f191c106c91..ae76a7eebe17 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -419,7 +419,8 @@ "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Rate", + "options": "currency" }, { "default": "0", @@ -647,7 +648,11 @@ "icon": "fa fa-gift", "idx": 1, "links": [], +<<<<<<< HEAD "modified": "2024-05-17 13:16:34.496704", +======= + "modified": "2024-09-16 18:14:51.314765", +>>>>>>> 636c0131fa (fix: add currency in options for rate field in pricing rule) "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", @@ -709,4 +714,4 @@ "sort_order": "DESC", "states": [], "title_field": "title" -} +} \ No newline at end of file From 82982e25c6758d2fdcf22c62bd61dcd065759fb4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 18 Sep 2024 12:10:42 +0530 Subject: [PATCH 40/40] chore: resolve conflict --- erpnext/accounts/doctype/pricing_rule/pricing_rule.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index ae76a7eebe17..ee9dd2be8c35 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -648,11 +648,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-05-17 13:16:34.496704", -======= "modified": "2024-09-16 18:14:51.314765", ->>>>>>> 636c0131fa (fix: add currency in options for rate field in pricing rule) "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule",