From c809e61103f84a42f23a1bb4a3d1fb56ebf5cff3 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Tue, 10 Oct 2023 20:40:50 +0200 Subject: [PATCH 01/57] feat(payment): add advance payment status to advance payment doctypes to better track advance payments --- .../doctype/payment_request/payment_request.py | 11 +++++++++++ .../doctype/purchase_order/purchase_order.json | 18 ++++++++++++++++-- .../doctype/purchase_order/purchase_order.py | 3 +++ erpnext/controllers/accounts_controller.py | 6 ++++++ .../doctype/sales_order/sales_order.json | 18 ++++++++++++++++-- .../selling/doctype/sales_order/sales_order.py | 2 ++ 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index df4f1b2c3f..e216d1476c 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -115,6 +115,17 @@ class PaymentRequest(Document): elif self.payment_channel == "Phone": self.request_phone_payment() + if ( + self.reference_doctype in ["Sales Order"] and ref_doc.advance_payment_status == "Not Requested" + ): + ref_doc.db_set("advance_payment_status", "Requested") + + if ( + self.reference_doctype in ["Purchase Order"] + and ref_doc.advance_payment_status == "Not Initiated" + ): + ref_doc.db_set("advance_payment_status", "Initiated") + def request_phone_payment(self): controller = _get_payment_gateway_controller(self.payment_gateway) request_amount = self.get_request_amount() diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index f74df6630e..f1ecdf5c00 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -134,6 +134,7 @@ "more_info_tab", "tracking_section", "status", + "advance_payment_status", "column_break_75", "per_billed", "per_received", @@ -1269,13 +1270,26 @@ "fieldtype": "Tab Break", "label": "Connections", "show_dashboard": 1 + }, + { + "default": "Not Initiated", + "fieldname": "advance_payment_status", + "fieldtype": "Select", + "hidden": 1, + "in_standard_filter": 1, + "label": "Advance Payment Status", + "no_copy": 1, + "oldfieldname": "status", + "oldfieldtype": "Select", + "options": "Not Initiated\nInitiated\nPartially Paid\nPaid", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-10-01 20:58:07.851037", + "modified": "2023-10-10 13:37:40.158761", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", @@ -1330,4 +1344,4 @@ "timeline_field": "supplier", "title_field": "supplier_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 465fe96b58..ca60348abb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -349,6 +349,9 @@ class PurchaseOrder(BuyingController): self.validate_budget() self.update_reserved_qty_for_subcontract() + if not self.advance_payment_status: + self.advance_payment_status = "Not Initiated" + frappe.get_doc("Authorization Control").validate_approving_authority( self.doctype, self.company, self.base_grand_total ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6812940ee2..42dd8471f4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1689,6 +1689,12 @@ class AccountsController(TransactionBase): ) frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid) + frappe.db.set_value( + self.doctype, + self.name, + "advance_payment_status", + "Partially Paid" if advance_paid < order_total else "Paid", + ) @property def company_abbr(self): diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index a74084d21f..a0468080a8 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -131,6 +131,7 @@ "per_billed", "per_picked", "billing_status", + "advance_payment_status", "sales_team_section_break", "sales_partner", "column_break7", @@ -1639,13 +1640,26 @@ "no_copy": 1, "print_hide": 1, "report_hide": 1 + }, + { + "default": "Not Requested", + "fieldname": "advance_payment_status", + "fieldtype": "Select", + "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, + "in_standard_filter": 1, + "label": "Advance Payment Status", + "no_copy": 1, + "options": "Not Requested\nRequested\nPartially Paid\nPaid", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-07-24 08:59:11.599875", + "modified": "2023-10-10 13:36:07.526793", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1723,4 +1737,4 @@ "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index aae0fee467..002ffe010f 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -88,6 +88,8 @@ class SalesOrder(SellingController): self.billing_status = "Not Billed" if not self.delivery_status: self.delivery_status = "Not Delivered" + if not self.advance_payment_status: + self.advance_payment_status = "Not Requested" self.reset_default_field_value("set_warehouse", "items", "warehouse") From e97af14ff4e9e7ee51748223e2f762b128d50835 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Tue, 10 Oct 2023 23:49:44 +0200 Subject: [PATCH 02/57] fixup! feat(payment): add advance payment status to advance payment doctypes to better track advance payments --- .../purchase_order/purchase_order.json | 1 - erpnext/patches.txt | 1 + .../v15_0/create_advance_payment_status.py | 54 +++++++++++++++++++ .../doctype/sales_order/sales_order.json | 1 - 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v15_0/create_advance_payment_status.py diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index f1ecdf5c00..97f2310c1a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1272,7 +1272,6 @@ "show_dashboard": 1 }, { - "default": "Not Initiated", "fieldname": "advance_payment_status", "fieldtype": "Select", "hidden": 1, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e9c056e3a9..4c7d8e5221 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -344,5 +344,6 @@ erpnext.patches.v15_0.delete_woocommerce_settings_doctype erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults erpnext.patches.v14_0.update_invoicing_period_in_subscription execute:frappe.delete_doc("Page", "welcome-to-erpnext") +erpnext.patches.v15_0.create_advance_payment_status # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v15_0/create_advance_payment_status.py b/erpnext/patches/v15_0/create_advance_payment_status.py new file mode 100644 index 0000000000..ff5ba8f2d5 --- /dev/null +++ b/erpnext/patches/v15_0/create_advance_payment_status.py @@ -0,0 +1,54 @@ +import frappe + + +def execute(): + """ + Description: + Calculate the new Advance Payment Statuse column in SO & PO + """ + + if frappe.reload_doc("selling", "doctype", "Sales Order"): + so = frappe.qb.DocType("Sales Order") + frappe.qb.update(so).set(so.advance_payment_status, "Not Requested").where( + so.docstatus == 1 + ).where(so.advance_paid == 0.0).run() + + frappe.qb.update(so).set(so.advance_payment_status, "Partially Paid").where( + so.docstatus == 1 + ).where(so.advance_payment_status.isnull()).where( + so.advance_paid < (so.rounded_total or so.grand_total) + ).run() + + frappe.qb.update(so).set(so.advance_payment_status, "Paid").where(so.docstatus == 1).where( + so.advance_payment_status.isnull() + ).where(so.advance_paid == (so.rounded_total or so.grand_total)).run() + + pr = frappe.qb.DocType("Payment Request") + frappe.qb.update(so).join(pr).on(so.name == pr.reference_name).set( + so.advance_payment_status, "Requested" + ).where(so.docstatus == 1).where(pr.docstatus == 1).where( + so.advance_payment_status == "Not Requested" + ).run() + + if frappe.reload_doc("buying", "doctype", "Purchase Order"): + po = frappe.qb.DocType("Purchase Order") + frappe.qb.update(po).set(po.advance_payment_status, "Not Initiated").where( + po.docstatus == 1 + ).where(po.advance_paid == 0.0).run() + + frappe.qb.update(po).set(po.advance_payment_status, "Partially Paid").where( + po.docstatus == 1 + ).where(po.advance_payment_status.isnull()).where( + po.advance_paid < (po.rounded_total or po.grand_total) + ).run() + + frappe.qb.update(po).set(po.advance_payment_status, "Paid").where(po.docstatus == 1).where( + po.advance_payment_status.isnull() + ).where(po.advance_paid == (po.rounded_total or po.grand_total)).run() + + pr = frappe.qb.DocType("Payment Request") + frappe.qb.update(po).join(pr).on(po.name == pr.reference_name).set( + po.advance_payment_status, "Initiated" + ).where(po.docstatus == 1).where(pr.docstatus == 1).where( + po.advance_payment_status == "Not Initiated" + ).run() diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index a0468080a8..7fb49a9b67 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1642,7 +1642,6 @@ "report_hide": 1 }, { - "default": "Not Requested", "fieldname": "advance_payment_status", "fieldtype": "Select", "hidden": 1, From 8b21ca2db917626d8392949a31d4e77766fa94c7 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sat, 14 Oct 2023 16:34:18 +0200 Subject: [PATCH 03/57] fixup! feat(payment): add advance payment status to advance payment doctypes to better track advance payments --- .../payment_request/payment_request.py | 37 +++++++++++++++++++ .../purchase_order/purchase_order_list.js | 4 +- .../purchase_order_analysis.js | 2 +- erpnext/controllers/status_updater.py | 16 ++++++-- .../doctype/sales_order/sales_order.json | 2 +- .../doctype/sales_order/sales_order_list.js | 4 +- .../payment_terms_status_for_sales_order.py | 2 +- .../sales_order_analysis.js | 2 +- 8 files changed, 59 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index e216d1476c..b690fed5b0 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -119,12 +119,16 @@ class PaymentRequest(Document): self.reference_doctype in ["Sales Order"] and ref_doc.advance_payment_status == "Not Requested" ): ref_doc.db_set("advance_payment_status", "Requested") + ref_doc.set_status(update=True) + ref_doc.notify_update() if ( self.reference_doctype in ["Purchase Order"] and ref_doc.advance_payment_status == "Not Initiated" ): ref_doc.db_set("advance_payment_status", "Initiated") + ref_doc.set_status(update=True) + ref_doc.notify_update() def request_phone_payment(self): controller = _get_payment_gateway_controller(self.payment_gateway) @@ -164,6 +168,39 @@ class PaymentRequest(Document): self.check_if_payment_entry_exists() self.set_as_cancelled() + if self.reference_doctype in ["Sales Order", "Purchase Order"]: + + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + if self.reference_doctype in ["Sales Order"] and ref_doc.advance_payment_status == "Requested": + peer_pr = frappe.db.count( + "Payment Request", + { + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + "docstatus": 1, + }, + ) + if not peer_pr: + ref_doc.db_set("advance_payment_status", "Not Requested") + ref_doc.set_status(update=True) + ref_doc.notify_update() + + if ( + self.reference_doctype in ["Purchase Order"] and ref_doc.advance_payment_status == "Initiated" + ): + peer_pr = frappe.db.count( + "Payment Request", + { + "reference_doctype": self.reference_doctype, + "reference_name": self.reference_name, + "docstatus": 1, + }, + ) + if not peer_pr: + ref_doc.db_set("advance_payment_status", "Not Initiated") + ref_doc.set_status(update=True) + ref_doc.notify_update() + def make_invoice(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart": diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index 6594746cfc..d39d7f9213 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -1,6 +1,6 @@ frappe.listview_settings['Purchase Order'] = { add_fields: ["base_grand_total", "company", "currency", "supplier", - "supplier_name", "per_received", "per_billed", "status"], + "supplier_name", "per_received", "per_billed", "status", "advance_payment_status"], get_indicator: function (doc) { if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; @@ -8,6 +8,8 @@ frappe.listview_settings['Purchase Order'] = { return [__("On Hold"), "orange", "status,=,On Hold"]; } else if (doc.status === "Delivered") { return [__("Delivered"), "green", "status,=,Closed"]; + } else if (doc.advance_payment_status == "Initiated") { + return [__("To Pay"), "gray", "advance_payment_status,=,Initiated"]; } else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") { if (flt(doc.per_billed, 2) < 100) { return [__("To Receive and Bill"), "orange", diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js index 91506c0ab3..3bf4f2bb29 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -54,7 +54,7 @@ frappe.query_reports["Purchase Order Analysis"] = { "fieldtype": "MultiSelectList", "width": "80", get_data: function(txt) { - let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"] + let status = ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"] let options = [] for (let option of status){ options.push({ diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 73a248fb53..2b79b895eb 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -53,6 +53,10 @@ status_map = { "To Deliver", "eval:self.per_delivered < 100 and self.per_billed == 100 and self.docstatus == 1 and not self.skip_delivery_note", ], + [ + "To Pay", + "eval:self.advance_payment_status == 'Requested' and self.docstatus == 1", + ], [ "Completed", "eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1", @@ -63,15 +67,19 @@ status_map = { ], "Purchase Order": [ ["Draft", None], - [ - "To Receive and Bill", - "eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1", - ], ["To Bill", "eval:self.per_received >= 100 and self.per_billed < 100 and self.docstatus == 1"], [ "To Receive", "eval:self.per_received < 100 and self.per_billed == 100 and self.docstatus == 1", ], + [ + "To Receive and Bill", + "eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1", + ], + [ + "To Pay", + "eval:self.advance_payment_status == 'Initiated' and self.docstatus == 1", + ], [ "Completed", "eval:self.per_received >= 100 and self.per_billed == 100 and self.docstatus == 1", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7fb49a9b67..084537eb4f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1270,7 +1270,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed", + "options": "\nDraft\nOn Hold\nTo Pay\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed", "print_hide": 1, "read_only": 1, "reqd": 1, diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 518f018726..37686a85c3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -1,6 +1,6 @@ frappe.listview_settings['Sales Order'] = { add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date", - "per_delivered", "per_billed", "status", "order_type", "name", "skip_delivery_note"], + "per_delivered", "per_billed", "status", "advance_payment_status", "order_type", "name", "skip_delivery_note"], get_indicator: function (doc) { if (doc.status === "Closed") { // Closed @@ -10,6 +10,8 @@ frappe.listview_settings['Sales Order'] = { return [__("On Hold"), "orange", "status,=,On Hold"]; } else if (doc.status === "Completed") { return [__("Completed"), "green", "status,=,Completed"]; + } else if (doc.advance_payment_status === "Requested") { + return [__("To Pay"), "gray", "advance_payment_status,=,Requested"]; } else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) { if (frappe.datetime.get_diff(doc.delivery_date) < 0) { // not delivered & overdue diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 3682c5fd62..00acc803d5 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -209,7 +209,7 @@ def get_so_with_invoices(filters): ) .where( (so.docstatus == 1) - & (so.status.isin(["To Deliver and Bill", "To Bill"])) + & (so.status.isin(["To Deliver and Bill", "To Bill", "To Pay"])) & (so.payment_terms_template != "NULL") & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js index ac3d3dbf71..fc685e0fe9 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js @@ -56,7 +56,7 @@ frappe.query_reports["Sales Order Analysis"] = { "fieldtype": "MultiSelectList", "width": "80", get_data: function(txt) { - let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"] + let status = ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"] let options = [] for (let option of status){ options.push({ From b5be17c6dfc9ab10e9eb57669f697fbb5576489c Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 8 Jan 2024 22:07:46 +0100 Subject: [PATCH 04/57] fix: use most reliable section reference per report line --- .../tax_withholding_details.py | 106 ++++++++++-------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index d045d91f52..4a80dd0339 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -46,12 +46,10 @@ def get_result( out = [] for name, details in gle_map.items(): - tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 - bill_no, bill_date = "", "" - tax_withholding_category = tax_category_map.get(name) - rate = tax_rate_map.get(tax_withholding_category) - for entry in details: + tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 + tax_withholding_category, rate = None, None + bill_no, bill_date = "", "" party = entry.party or entry.against posting_date = entry.posting_date voucher_type = entry.voucher_type @@ -61,12 +59,19 @@ def get_result( if party_list: party = party_list[0] - if not tax_withholding_category: - tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) - - if entry.account in tds_accounts: + if entry.account in tds_accounts.keys(): tax_amount += entry.credit - entry.debit + # infer tax withholding category from the account if it's the single account for this category + tax_withholding_category = tds_accounts.get(entry.account) + rate = tax_rate_map.get(tax_withholding_category) + # or else the consolidated value from the voucher document + if not tax_withholding_category: + # or else from the party default + tax_withholding_category = tax_category_map.get(name) + rate = tax_rate_map.get(tax_withholding_category) + if not tax_withholding_category: + tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") + rate = tax_rate_map.get(tax_withholding_category) if net_total_map.get(name): if voucher_type == "Journal Entry" and tax_amount and rate: @@ -80,41 +85,41 @@ def get_result( else: total_amount += entry.credit - if tax_amount: - if party_map.get(party, {}).get("party_type") == "Supplier": - party_name = "supplier_name" - party_type = "supplier_type" - else: - party_name = "customer_name" - party_type = "customer_type" + if tax_amount: + if party_map.get(party, {}).get("party_type") == "Supplier": + party_name = "supplier_name" + party_type = "supplier_type" + else: + party_name = "customer_name" + party_type = "customer_type" - row = { - "pan" - if frappe.db.has_column(filters.party_type, "pan") - else "tax_id": party_map.get(party, {}).get("pan"), - "party": party_map.get(party, {}).get("name"), - } - - if filters.naming_series == "Naming Series": - row.update({"party_name": party_map.get(party, {}).get(party_name)}) - - row.update( - { - "section_code": tax_withholding_category or "", - "entity_type": party_map.get(party, {}).get(party_type), - "rate": rate, - "total_amount": total_amount, - "grand_total": grand_total, - "base_total": base_total, - "tax_amount": tax_amount, - "transaction_date": posting_date, - "transaction_type": voucher_type, - "ref_no": name, - "supplier_invoice_no": bill_no, - "supplier_invoice_date": bill_date, + row = { + "pan" + if frappe.db.has_column(filters.party_type, "pan") + else "tax_id": party_map.get(party, {}).get("pan"), + "party": party_map.get(party, {}).get("name"), } - ) - out.append(row) + + if filters.naming_series == "Naming Series": + row.update({"party_name": party_map.get(party, {}).get(party_name)}) + + row.update( + { + "section_code": tax_withholding_category or "", + "entity_type": party_map.get(party, {}).get(party_type), + "rate": rate, + "total_amount": total_amount, + "grand_total": grand_total, + "base_total": base_total, + "tax_amount": tax_amount, + "transaction_date": posting_date, + "transaction_type": voucher_type, + "ref_no": name, + "supplier_invoice_no": bill_no, + "supplier_invoice_date": bill_date, + } + ) + out.append(row) out.sort(key=lambda x: x["section_code"]) @@ -282,11 +287,20 @@ def get_tds_docs(filters): journal_entry_party_map = frappe._dict() bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") - tds_accounts = frappe.get_all( - "Tax Withholding Account", {"company": filters.get("company")}, pluck="account" + _tds_accounts = frappe.get_all( + "Tax Withholding Account", + {"company": filters.get("company")}, + ["account", "parent"], ) + tds_accounts = {} + for tds_acc in _tds_accounts: + # if it turns out not to be the only tax withholding category, then don't include in the map + if tds_accounts.get(tds_acc["account"]): + tds_accounts[tds_acc["account"]] = None + else: + tds_accounts[tds_acc["account"]] = tds_acc["parent"] - tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True) + tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True) for d in tds_docs: if d.voucher_type == "Purchase Invoice": From 6c8f52b26f0601135ec0dd1be94835951936cd11 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 11 Jan 2024 16:00:48 +0100 Subject: [PATCH 05/57] fix: Payment Terms Status for Sales Order report should show all payment terms from order not only this comming from template --- .../payment_terms_status_for_sales_order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 3682c5fd62..c6e4647538 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -210,7 +210,6 @@ def get_so_with_invoices(filters): .where( (so.docstatus == 1) & (so.status.isin(["To Deliver and Bill", "To Bill"])) - & (so.payment_terms_template != "NULL") & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) ) From 1cde804c773de41520a6148e7d99ab0c23c39ae1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 30 Nov 2023 13:11:11 +0530 Subject: [PATCH 06/57] refactor: dimensions section in allocation table in reconciliation --- .../payment_reconciliation_allocation.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 491c67818d..cbfd9b2d8b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -24,6 +24,7 @@ "difference_account", "exchange_rate", "currency", + "accounting_dimensions_section", "cost_center" ], "fields": [ @@ -157,12 +158,17 @@ "fieldname": "gain_loss_posting_date", "fieldtype": "Date", "label": "Difference Posting Date" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" } ], "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-11-17 17:33:38.612615", + "modified": "2023-12-14 12:32:45.554730", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", From cfb3d872673844f04f5c9dd3f7d7f56288e5dd22 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Dec 2023 12:16:50 +0530 Subject: [PATCH 07/57] refactor: update dimension doctypes in hooks --- erpnext/hooks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6efb893e63..cd588c87a6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -538,6 +538,8 @@ accounting_dimension_doctypes = [ "Account Closing Balance", "Supplier Quotation", "Supplier Quotation Item", + "Payment Reconciliation", + "Payment Reconciliation Allocation", ] get_matching_queries = ( From 20e0acc20a218029d7101a1ba6ff3c1ae03fac02 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Dec 2023 12:39:13 +0530 Subject: [PATCH 08/57] refactor: dimensions filter section in payment reconciliation --- .../payment_reconciliation.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index ccb9e648cb..bab79d311d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -25,6 +25,7 @@ "invoice_limit", "payment_limit", "bank_cash_account", + "accounting_dimensions_section", "cost_center", "sec_break1", "invoice_name", @@ -208,6 +209,14 @@ "fieldname": "payment_name", "fieldtype": "Data", "label": "Filter on Payment" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: doc.invoices.length == 0", + "depends_on": "eval:doc.receivable_payable_account", + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions Filter" } ], "hide_toolbar": 1, @@ -215,7 +224,7 @@ "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-11-17 17:33:55.701726", + "modified": "2023-12-14 12:38:44.910625", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", From 20576e0f47ba3c4937121bfab1e0d8d395a590ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Dec 2023 13:33:24 +0530 Subject: [PATCH 09/57] refactor: column break in dimension section --- .../payment_reconciliation/payment_reconciliation.json | 7 ++++++- .../payment_reconciliation_allocation.json | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index bab79d311d..666926f00e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -27,6 +27,7 @@ "bank_cash_account", "accounting_dimensions_section", "cost_center", + "dimension_col_break", "sec_break1", "invoice_name", "invoices", @@ -217,6 +218,10 @@ "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions Filter" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, @@ -224,7 +229,7 @@ "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-12-14 12:38:44.910625", + "modified": "2023-12-14 13:38:16.264013", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index cbfd9b2d8b..3f85b21350 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -25,7 +25,8 @@ "exchange_rate", "currency", "accounting_dimensions_section", - "cost_center" + "cost_center", + "dimension_col_break" ], "fields": [ { @@ -163,12 +164,16 @@ "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-12-14 12:32:45.554730", + "modified": "2023-12-14 13:38:26.104150", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", From c1fe4bcc64775507a3bd8e02b61274d8dc2d6447 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Dec 2023 16:02:12 +0530 Subject: [PATCH 10/57] refactor: handle dimension filters --- .../payment_reconciliation/payment_reconciliation.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index ed0921ba5b..092cf45100 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( is_any_doc_running, ) @@ -648,6 +649,14 @@ class PaymentReconciliation(Document): if not invoices_to_reconcile: frappe.throw(_("No records found in Allocation table")) + def build_dimensions_filter_conditions(self): + ple = qb.DocType("Payment Ledger Entry") + dimensions_and_defaults = get_dimensions() + for x in dimensions_and_defaults[0]: + dimension = x.fieldname + if self.get(dimension): + self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) + def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): self.common_filter_conditions.clear() self.accounting_dimension_filter_conditions.clear() @@ -671,6 +680,8 @@ class PaymentReconciliation(Document): if self.to_payment_date: self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date)) + self.build_dimensions_filter_conditions() + def get_conditions(self, get_payments=False): condition = " and company = '{0}' ".format(self.company) From ff60ec85b85d5548886e247b72cf1262587feba3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Dec 2023 17:25:02 +0530 Subject: [PATCH 11/57] refactor: pass dimension filters to query --- .../payment_reconciliation.py | 9 +++ erpnext/controllers/accounts_controller.py | 69 ++++++++----------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 092cf45100..cb7d5ea951 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -173,6 +173,15 @@ class PaymentReconciliation(Document): if self.payment_name: condition.update({"name": self.payment_name}) + # pass dynamic dimension filter values to query builder + dimensions = {} + dimensions_and_defaults = get_dimensions() + for x in dimensions_and_defaults[0]: + dimension = x.fieldname + if self.get(dimension): + dimensions.update({dimension: self.get(dimension)}) + condition.update({"accounting_dimensions": dimensions}) + payment_entries = get_advance_payment_entries_for_regional( self.party_type, self.party, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index aef3d31b10..31ff79906f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +from frappe.query_builder import Criterion from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( @@ -2695,47 +2696,37 @@ def get_common_query( q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) if condition: - if condition.get("name", None): - q = q.where(payment_entry.name.like(f"%{condition.get('name')}%")) + # conditions should be built as an array and passed as Criterion + common_filter_conditions = [] + + common_filter_conditions.append(payment_entry.company == condition["company"]) + if condition.get("name", None): + common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%")) + + if condition.get("from_payment_date"): + common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"])) + + if condition.get("to_payment_date"): + common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"])) - q = q.where(payment_entry.company == condition["company"]) - q = ( - q.where(payment_entry.posting_date >= condition["from_payment_date"]) - if condition.get("from_payment_date") - else q - ) - q = ( - q.where(payment_entry.posting_date <= condition["to_payment_date"]) - if condition.get("to_payment_date") - else q - ) if condition.get("get_payments") == True: - q = ( - q.where(payment_entry.cost_center == condition["cost_center"]) - if condition.get("cost_center") - else q - ) - q = ( - q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"]) - if condition.get("minimum_payment_amount") - else q - ) - q = ( - q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"]) - if condition.get("maximum_payment_amount") - else q - ) - else: - q = ( - q.where(payment_entry.total_debit >= condition["minimum_payment_amount"]) - if condition.get("minimum_payment_amount") - else q - ) - q = ( - q.where(payment_entry.total_debit <= condition["maximum_payment_amount"]) - if condition.get("maximum_payment_amount") - else q - ) + if condition.get("cost_center"): + common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"]) + + if condition.get("accounting_dimensions"): + for field, val in condition.get("accounting_dimensions").items(): + common_filter_conditions.append(payment_entry[field] == val) + + if condition.get("minimum_payment_amount"): + common_filter_conditions.append( + payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"]) + ) + + if condition.get("maximum_payment_amount"): + common_filter_conditions.append( + payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"]) + ) + q = q.where(Criterion.all(common_filter_conditions)) q = q.orderby(payment_entry.posting_date) q = q.limit(limit) if limit else q From ad8475cb8b24d40b04f86903feee08ecac6aa1f1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 19 Dec 2023 17:07:54 +0530 Subject: [PATCH 12/57] refactor: set query filters for dimensions --- .../payment_reconciliation.js | 21 +++++++++++++++++++ .../payment_reconciliation.py | 17 +++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index fc90c3dec0..99593defae 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -95,6 +95,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.change_custom_button_type(__('Allocate'), null, 'default'); } + this.frm.trigger("set_query_for_dimension_filters"); + // check for any running reconciliation jobs if (this.frm.doc.receivable_payable_account) { this.frm.call({ @@ -125,6 +127,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } } + set_query_for_dimension_filters() { + frappe.call({ + method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters", + args: { + company: this.frm.doc.company, + }, + callback: (r) => { + if (!r.exc && r.message) { + r.message.forEach(x => { + this.frm.set_query(x.fieldname, () => { + return { + 'filters': x.filters + }; + }); + }); + } + } + }); + } company() { this.frm.set_value('party', ''); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index cb7d5ea951..83bccf4271 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -813,3 +813,20 @@ def reconcile_dr_cr_note(dr_cr_notes, company): @erpnext.allow_regional def adjust_allocations_for_taxes(doc): pass + + +@frappe.whitelist() +def get_queries_for_dimension_filters(company: str = None): + dimensions_with_filters = [] + for d in get_dimensions()[0]: + filters = {} + meta = frappe.get_meta(d.document_type) + if meta.has_field("company") and company: + filters.update({"company": company}) + + if meta.is_tree: + filters.update({"is_group": 0}) + + dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters}) + + return dimensions_with_filters From 5dc22e1811bb1841bb8c790cc3a1e1315cef6074 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Dec 2023 17:19:27 +0530 Subject: [PATCH 13/57] refactor: pass dimension details to query --- .../payment_reconciliation.py | 17 ++++++++++++----- erpnext/accounts/utils.py | 2 ++ erpnext/controllers/accounts_controller.py | 2 ++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 83bccf4271..f382434eb7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -71,6 +71,7 @@ class PaymentReconciliation(Document): self.common_filter_conditions = [] self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] + self.dimensions = get_dimensions()[0] def load_from_db(self): # 'modified' attribute is required for `run_doc_method` to work properly. @@ -175,8 +176,7 @@ class PaymentReconciliation(Document): # pass dynamic dimension filter values to query builder dimensions = {} - dimensions_and_defaults = get_dimensions() - for x in dimensions_and_defaults[0]: + for x in self.dimensions: dimension = x.fieldname if self.get(dimension): dimensions.update({dimension: self.get(dimension)}) @@ -528,7 +528,7 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() def get_payment_details(self, row, dr_or_cr): - return frappe._dict( + payment_details = frappe._dict( { "voucher_type": row.get("reference_type"), "voucher_no": row.get("reference_name"), @@ -551,6 +551,14 @@ class PaymentReconciliation(Document): } ) + dimensions_dict = {} + for x in self.dimensions: + if row.get(x.fieldname): + dimensions_dict.update({x.fieldname: row.get(x.fieldname)}) + + payment_details.update({"dimensions": dimensions_dict}) + return payment_details + def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: if not self.get(fieldname): @@ -660,8 +668,7 @@ class PaymentReconciliation(Document): def build_dimensions_filter_conditions(self): ple = qb.DocType("Payment Ledger Entry") - dimensions_and_defaults = get_dimensions() - for x in dimensions_and_defaults[0]: + for x in self.dimensions: dimension = x.fieldname if self.get(dimension): self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f933209364..5fffa270f5 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -668,6 +668,7 @@ def update_reference_in_payment_entry( else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.difference_amount, "account": d.account, + "dimensions": d.dimensions, } if d.voucher_detail_no: @@ -2043,6 +2044,7 @@ def create_gain_loss_journal( ref2_dn, ref2_detail_no, cost_center, + dimensions, ) -> str: journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 31ff79906f..f8d53d8082 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1275,6 +1275,7 @@ class AccountsController(TransactionBase): self.name, arg.get("referenced_row"), arg.get("cost_center"), + {}, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( @@ -1355,6 +1356,7 @@ class AccountsController(TransactionBase): self.name, d.idx, self.cost_center, + {}, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( From 9c5a79209eb014c90aac46a5dd5ed0d9b7cb8f87 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Dec 2023 17:42:41 +0530 Subject: [PATCH 14/57] refactor: replace sql with query builder for Jourals query --- .../payment_reconciliation.py | 133 ++++++++---------- 1 file changed, 61 insertions(+), 72 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index f382434eb7..26bf1c0605 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -195,66 +195,67 @@ class PaymentReconciliation(Document): return payment_entries def get_jv_entries(self): - condition = self.get_conditions() + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + conditions = self.get_journal_filter_conditions() + + # Dimension filters + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + conditions.append(jea[dimension] == self.get(dimension)) if self.payment_name: - condition += f" and t1.name like '%%{self.payment_name}%%'" + conditions.append(je.name.like(f"%%{self.payent_name}%%")) if self.get("cost_center"): - condition += f" and t2.cost_center = '{self.cost_center}' " + conditions.append(jea.cost_center == self.cost_center) dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit_in_account_currency" ) + conditions.append(jea[dr_or_cr].gt(0)) - bank_account_condition = ( - "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" + if self.bank_cash_account: + conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%")) + + journal_query = ( + qb.from_(je) + .inner_join(jea) + .on(jea.parent == je.name) + .select( + ConstantColumn("Journal Entry").as_("reference_type"), + je.name.as_("reference_name"), + je.posting_date, + je.remark.as_("remarks"), + jea.name.as_("reference_row"), + jea[dr_or_cr].as_("amount"), + jea.is_advance, + jea.exchange_rate, + jea.account_currency.as_("currency"), + jea.cost_center.as_("cost_center"), + ) + .where( + (je.docstatus == 1) + & (jea.party_type == self.party_type) + & (jea.party == self.party) + & (jea.account == self.receivable_payable_account) + & ( + (jea.reference_type == "") + | (jea.reference_type.isnull()) + | (jea.reference_type.isin(("Sales Order", "Purchase Order"))) + ) + ) + .where(Criterion.all(conditions)) + .orderby(je.posting_date) ) - limit = f"limit {self.payment_limit}" if self.payment_limit else " " + if self.payment_limit: + journal_query = journal_query.limit(self.payment_limit) - # nosemgrep - journal_entries = frappe.db.sql( - """ - select - "Journal Entry" as reference_type, t1.name as reference_name, - t1.posting_date, t1.remark as remarks, t2.name as reference_row, - {dr_or_cr} as amount, t2.is_advance, t2.exchange_rate, - t2.account_currency as currency, t2.cost_center as cost_center - from - `tabJournal Entry` t1, `tabJournal Entry Account` t2 - where - t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1 - and t2.party_type = %(party_type)s and t2.party = %(party)s - and t2.account = %(account)s and {dr_or_cr} > 0 {condition} - and (t2.reference_type is null or t2.reference_type = '' or - (t2.reference_type in ('Sales Order', 'Purchase Order') - and t2.reference_name is not null and t2.reference_name != '')) - and (CASE - WHEN t1.voucher_type in ('Debit Note', 'Credit Note') - THEN 1=1 - ELSE {bank_account_condition} - END) - order by t1.posting_date - {limit} - """.format( - **{ - "dr_or_cr": dr_or_cr, - "bank_account_condition": bank_account_condition, - "condition": condition, - "limit": limit, - } - ), - { - "party_type": self.party_type, - "party": self.party, - "account": self.receivable_payable_account, - "bank_cash_account": "%%%s%%" % self.bank_cash_account, - }, - as_dict=1, - ) + journal_entries = journal_query.run(as_dict=True) return list(journal_entries) @@ -698,37 +699,25 @@ class PaymentReconciliation(Document): self.build_dimensions_filter_conditions() - def get_conditions(self, get_payments=False): - condition = " and company = '{0}' ".format(self.company) + def get_journal_filter_conditions(self): + conditions = [] + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + conditions.append(je.company == self.company) - if self.get("cost_center") and get_payments: - condition = " and cost_center = '{0}' ".format(self.cost_center) + if self.from_payment_date: + conditions.append(je.posting_date.gte(self.from_payment_date)) - condition += ( - " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) - if self.from_payment_date - else "" - ) - condition += ( - " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) - if self.to_payment_date - else "" - ) + if self.to_payment_date: + conditions.append(je.posting_date.lte(self.to_payment_date)) if self.minimum_payment_amount: - condition += ( - " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) - if get_payments - else " and total_debit >= {0}".format(flt(self.minimum_payment_amount)) - ) - if self.maximum_payment_amount: - condition += ( - " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) - if get_payments - else " and total_debit <= {0}".format(flt(self.maximum_payment_amount)) - ) + conditions.append(je.total_debit.gte(self.minimum_payment_amount)) - return condition + if self.maximum_payment_amount: + conditions.append(je.total_debit.lte(self.maximumb_payment_amount)) + + return conditions def reconcile_dr_cr_note(dr_cr_notes, company): From 2154502955166243e354897d7dcb22d1987c4693 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 2 Jan 2024 11:33:02 +0530 Subject: [PATCH 15/57] refactor: partial change on outstanding invoice popup --- .../doctype/payment_entry/payment_entry.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9402e3da09..a98934a664 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -638,6 +638,20 @@ frappe.ui.form.on('Payment Entry', { frm.events.set_unallocated_amount(frm); }, + get_dimensions: function(frm) { + let result = []; + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + async: false, + callback: function(r) { + if(!r.exc) { + result = r.message[0].map(elem => elem.document_type); + } + } + }); + return result; + }, + get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { const today = frappe.datetime.get_today(); const fields = [ From 0ec17590ae062fbda0c14a2806ec1ac07c638593 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jan 2024 14:56:51 +0530 Subject: [PATCH 16/57] fix: typo's and parameter changes --- .../doctype/payment_reconciliation/payment_reconciliation.py | 5 +++-- erpnext/accounts/utils.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 26bf1c0605..81601a2196 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -206,7 +206,7 @@ class PaymentReconciliation(Document): conditions.append(jea[dimension] == self.get(dimension)) if self.payment_name: - conditions.append(je.name.like(f"%%{self.payent_name}%%")) + conditions.append(je.name.like(f"%%{self.payment_name}%%")) if self.get("cost_center"): conditions.append(jea.cost_center == self.cost_center) @@ -715,7 +715,7 @@ class PaymentReconciliation(Document): conditions.append(je.total_debit.gte(self.minimum_payment_amount)) if self.maximum_payment_amount: - conditions.append(je.total_debit.lte(self.maximumb_payment_amount)) + conditions.append(je.total_debit.lte(self.maximum_payment_amount)) return conditions @@ -803,6 +803,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.against_voucher, None, inv.cost_center, + frappe._dict(), ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5fffa270f5..a80cf6fb1f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2046,6 +2046,8 @@ def create_gain_loss_journal( cost_center, dimensions, ) -> str: + # TODO: pass dimensions to Journal + journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = company From ab939cc6e8ab3669f1e9b0f007e9459be180ac32 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jan 2024 16:13:26 +0530 Subject: [PATCH 17/57] refactor: Credit Note and its Exc gain/loss JE inherits dimensions --- .../payment_reconciliation.py | 31 ++++++++++++++----- erpnext/accounts/utils.py | 6 ++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 81601a2196..be7201b32f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -458,8 +458,15 @@ class PaymentReconciliation(Document): row = self.append("allocation", {}) row.update(entry) + def update_dimension_values_in_allocated_entries(self, res): + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + res[dimension] = self.get(dimension) + return res + def get_allocated_entry(self, pay, inv, allocated_amount): - return frappe._dict( + res = frappe._dict( { "reference_type": pay.get("reference_type"), "reference_name": pay.get("reference_name"), @@ -475,6 +482,9 @@ class PaymentReconciliation(Document): } ) + res = self.update_dimension_values_in_allocated_entries(res) + return res + def reconcile_allocations(self, skip_ref_details_update_for_pe=False): adjust_allocations_for_taxes(self) dr_or_cr = ( @@ -500,7 +510,7 @@ class PaymentReconciliation(Document): reconcile_against_document(entry_list, skip_ref_details_update_for_pe) if dr_or_cr_notes: - reconcile_dr_cr_note(dr_or_cr_notes, self.company) + reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions) @frappe.whitelist() def reconcile(self): @@ -552,12 +562,10 @@ class PaymentReconciliation(Document): } ) - dimensions_dict = {} for x in self.dimensions: if row.get(x.fieldname): - dimensions_dict.update({x.fieldname: row.get(x.fieldname)}) + payment_details[x.fieldname] = row.get(x.fieldname) - payment_details.update({"dimensions": dimensions_dict}) return payment_details def check_mandatory_to_fetch(self): @@ -720,7 +728,7 @@ class PaymentReconciliation(Document): return conditions -def reconcile_dr_cr_note(dr_cr_notes, company): +def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None): for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" @@ -770,6 +778,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company): } ) + # Credit Note(JE) will inherit the same dimension values as payment + dimensions_dict = frappe._dict() + if active_dimensions: + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = inv.get(dim.fieldname) + + jv.accounts[0].update(dimensions_dict) + jv.accounts[1].update(dimensions_dict) + jv.flags.ignore_mandatory = True jv.flags.ignore_exchange_rate = True jv.remark = None @@ -803,7 +820,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.against_voucher, None, inv.cost_center, - frappe._dict(), + dimensions_dict, ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a80cf6fb1f..9f7e89a189 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2046,8 +2046,6 @@ def create_gain_loss_journal( cost_center, dimensions, ) -> str: - # TODO: pass dimensions to Journal - journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = company @@ -2080,7 +2078,7 @@ def create_gain_loss_journal( dr_or_cr + "_in_account_currency": 0, } ) - + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_account = frappe._dict( @@ -2096,7 +2094,7 @@ def create_gain_loss_journal( reverse_dr_or_cr: abs(exc_gain_loss), } ) - + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_entry.save() From 73625a2622f29a83c98179c0b5048a29e61ce243 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:25:03 +0530 Subject: [PATCH 18/57] Revert "fix(minor): financial statements period end date" --- erpnext/accounts/report/financial_statements.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 004a9299ea..aadd8731ca 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -8,17 +8,7 @@ import re import frappe from frappe import _ -from frappe.utils import ( - add_days, - add_months, - cint, - cstr, - flt, - formatdate, - get_first_day, - getdate, - today, -) +from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -53,8 +43,6 @@ def get_period_list( year_start_date = getdate(period_start_date) year_end_date = getdate(period_end_date) - year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date - months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] period_list = [] From 188ff8cde794bb1ef1043f0e47469d65944aac1e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 17 Jan 2024 12:37:43 +0530 Subject: [PATCH 19/57] refactor: apply dimension filters on cr/dr notes --- .../doctype/payment_reconciliation/payment_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index be7201b32f..e5a34e21ab 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -309,6 +309,7 @@ class PaymentReconciliation(Document): min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None, get_payments=True, + accounting_dimensions=self.accounting_dimension_filter_conditions, ) for inv in return_outstanding: From e3c44231abbbe389a1f815ab77f2d6ff0c614e1b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 17 Jan 2024 12:50:05 +0530 Subject: [PATCH 20/57] chore: test dimension filter output --- .../tests/test_accounts_controller.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 97d3c5c32d..3a3e6def48 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -56,6 +56,7 @@ class TestAccountsController(FrappeTestCase): 20 series - Sales Invoice against Journals 30 series - Sales Invoice against Credit Notes 40 series - Company default Cost center is unset + 50 series - Dimension inheritence """ def setUp(self): @@ -1255,3 +1256,88 @@ class TestAccountsController(FrappeTestCase): ) frappe.db.set_value("Company", self.company, "cost_center", cc) + + def setup_dimensions(self): + # create dimension + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + ) + + create_dimension() + # make it non-mandatory + loc = frappe.get_doc("Accounting Dimension", "Location") + for x in loc.dimension_defaults: + x.mandatory_for_bs = False + x.mandatory_for_pl = False + loc.save() + + def test_50_dimensions_filter(self): + """ + Gain/Loss JE should inherit its dimension from payment + """ + self.setup_dimensions() + rate_in_account_currency = 1 + + # Invoices + si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si1.department = "Management" + si1.save().submit() + + si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si2.department = "Operations" + si2.save().submit() + + # Payments + cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note1.department = "Management" + cr_note1.is_return = 1 + cr_note1.save().submit() + + cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note2.department = "Legal" + cr_note2.is_return = 1 + cr_note2.save().submit() + + pe1 = get_payment_entry(si1.doctype, si1.name) + pe1.references = [] + pe1.department = "Research & Development" + pe1.save().submit() + + pe2 = get_payment_entry(si1.doctype, si1.name) + pe2.references = [] + pe2.department = "Management" + pe2.save().submit() + + je1 = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + je1.accounts[0].party_type = "Customer" + je1.accounts[0].party = self.customer + je1.accounts[0].department = "Management" + je1.save().submit() + + # assert dimension filter's result + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 2) + self.assertEqual(len(pr.payments), 5) + + pr.department = "Legal" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 1) + + pr.department = "Management" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 3) + + pr.department = "Research & Development" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 1) From 8b9128703406c1af12f994619f90e6302bae21d5 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 17 Jan 2024 13:09:51 +0530 Subject: [PATCH 21/57] fix: account and stock manager read perm --- .../accounts/doctype/fiscal_year/fiscal_year.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 5ab91f2506..bd2bfbd381 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -82,11 +82,11 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2020-11-05 12:16:53.081573", + "modified": "2024-01-17 13:06:01.608953", "modified_by": "Administrator", "module": "Accounts", "name": "Fiscal Year", - "owner": "Administrator", + "owner": "Administrator", "permissions": [ { "create": 1, @@ -118,6 +118,14 @@ { "read": 1, "role": "Employee" + }, + { + "read": 1, + "role": "Accounts Manager" + }, + { + "read": 1, + "role": "Stock Manager" } ], "show_name_in_global_search": 1, From ba5a7c8cd8ee6fc09b0d81ffbe8b364e584f1f1b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jan 2024 13:20:06 +0530 Subject: [PATCH 22/57] test: dimension inheritance for cr note reconciliation --- .../tests/test_accounts_controller.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 3a3e6def48..a448ad4a57 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1273,7 +1273,7 @@ class TestAccountsController(FrappeTestCase): def test_50_dimensions_filter(self): """ - Gain/Loss JE should inherit its dimension from payment + Test workings of dimension filters """ self.setup_dimensions() rate_in_account_currency = 1 @@ -1341,3 +1341,45 @@ class TestAccountsController(FrappeTestCase): pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) + + def test_51_cr_note_should_inherit_dimension_from_payment(self): + self.setup_dimensions() + rate_in_account_currency = 1 + + # Invoice + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si.department = "Management" + si.save().submit() + + # Payment + cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note.department = "Management" + cr_note.is_return = 1 + cr_note.save().submit() + + pr = self.create_payment_reconciliation() + pr.department = "Management" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be 2 journals, JE(Cr Note) and JE(Exchange Gain/Loss) + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_cr_note), 2) + self.assertEqual(exc_je_for_si, exc_je_for_cr_note) + + for x in exc_je_for_si + exc_je_for_cr_note: + with self.subTest(x=x): + self.assertEqual( + [cr_note.department, cr_note.department], + frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"), + ) From c44eb432a59fb3ffb3748e47356068499f1129b1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jan 2024 14:35:06 +0530 Subject: [PATCH 23/57] refactor: pass dimension values to Gain/Loss journal --- .../payment_reconciliation.py | 2 +- erpnext/accounts/utils.py | 29 +++++++++++++++---- erpnext/controllers/accounts_controller.py | 8 +++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index e5a34e21ab..b2716c9da4 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -508,7 +508,7 @@ class PaymentReconciliation(Document): reconciled_entry.append(payment_details) if entry_list: - reconcile_against_document(entry_list, skip_ref_details_update_for_pe) + reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions) if dr_or_cr_notes: reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9f7e89a189..d688544122 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -453,7 +453,19 @@ def add_cc(args=None): return cc.name -def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep +def _build_dimensions_dict_for_exc_gain_loss( + entry: dict | object = None, active_dimensions: list = None +): + dimensions_dict = frappe._dict() + if entry and active_dimensions: + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = entry.get(dim.fieldname) + return dimensions_dict + + +def reconcile_against_document( + args, skip_ref_details_update_for_pe=False, active_dimensions=None +): # nosemgrep """ Cancel PE or JV, Update against document, split if required and resubmit """ @@ -482,6 +494,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n check_if_advance_entry_modified(entry) validate_allocated_amount(entry) + dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions) + # update ref in advance entry if voucher_type == "Journal Entry": referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False) @@ -489,10 +503,14 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # amount and account in args # referenced_row is used to deduplicate gain/loss journal entry.update({"referenced_row": referenced_row}) - doc.make_exchange_gain_loss_journal([entry]) + doc.make_exchange_gain_loss_journal([entry], dimensions_dict) else: referenced_row = update_reference_in_payment_entry( - entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe + entry, + doc, + do_not_save=True, + skip_ref_details_update_for_pe=skip_ref_details_update_for_pe, + dimensions_dict=dimensions_dict, ) doc.save(ignore_permissions=True) @@ -655,7 +673,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): def update_reference_in_payment_entry( - d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False + d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False, dimensions_dict=None ): reference_details = { "reference_doctype": d.against_voucher_type, @@ -701,8 +719,9 @@ def update_reference_in_payment_entry( if not skip_ref_details_update_for_pe: payment_entry.set_missing_ref_details() payment_entry.set_amounts() + payment_entry.make_exchange_gain_loss_journal( - frappe._dict({"difference_posting_date": d.difference_posting_date}) + frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict ) if not do_not_save: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f8d53d8082..1696df1cf0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1220,7 +1220,9 @@ class AccountsController(TransactionBase): return True return False - def make_exchange_gain_loss_journal(self, args: dict = None) -> None: + def make_exchange_gain_loss_journal( + self, args: dict = None, dimensions_dict: dict = None + ) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ @@ -1275,7 +1277,7 @@ class AccountsController(TransactionBase): self.name, arg.get("referenced_row"), arg.get("cost_center"), - {}, + dimensions_dict, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( @@ -1356,7 +1358,7 @@ class AccountsController(TransactionBase): self.name, d.idx, self.cost_center, - {}, + dimensions_dict, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( From a50808a077b8d6475ff360b611cd5f0a8cb37d65 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jan 2024 18:03:16 +0530 Subject: [PATCH 24/57] refactor: delete transactions in background --- erpnext/setup/doctype/company/company.js | 66 ++++++++++++++---------- erpnext/setup/doctype/company/company.py | 34 +++++++++++- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 1bd469b956..340a917ffa 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -140,38 +140,48 @@ frappe.ui.form.on("Company", { }, delete_company_transactions: function(frm) { - frappe.verify_password(function() { - var d = frappe.prompt({ - fieldtype:"Data", - fieldname: "company_name", - label: __("Please enter the company name to confirm"), - reqd: 1, - description: __("Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.") + frappe.call({ + method: "erpnext.setup.doctype.company.company.is_deletion_job_running", + args: { + company: frm.doc.name }, - function(data) { - if(data.company_name !== frm.doc.name) { - frappe.msgprint(__("Company name not same")); - return; + freeze: true, + callback: function(r) { + if(!r.exc) { + frappe.verify_password(function() { + var d = frappe.prompt({ + fieldtype:"Data", + fieldname: "company_name", + label: __("Please enter the company name to confirm"), + reqd: 1, + description: __("Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.") + }, + function(data) { + if(data.company_name !== frm.doc.name) { + frappe.msgprint(__("Company name not same")); + return; + } + frappe.call({ + method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request", + args: { + company: data.company_name + }, + freeze: true, + callback: function(r, rt) { }, + onerror: function() { + frappe.msgprint(__("Wrong Password")); + } + }); + }, + __("Delete all the Transactions for this Company"), __("Delete") + ); + d.get_primary_btn().addClass("btn-danger"); + }); } - frappe.call({ - method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request", - args: { - company: data.company_name - }, - freeze: true, - callback: function(r, rt) { - if(!r.exc) - frappe.msgprint(__("Successfully deleted all transactions related to this company!")); - }, - onerror: function() { - frappe.msgprint(__("Wrong Password")); - } - }); + }, - __("Delete all the Transactions for this Company"), __("Delete") - ); - d.get_primary_btn().addClass("btn-danger"); }); + } }); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index ec953b885e..68a3854b0d 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -11,7 +11,8 @@ from frappe.cache_manager import clear_defaults_cache from frappe.contacts.address_and_contact import load_address_and_contact from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.desk.page.setup_wizard.setup_wizard import make_records -from frappe.utils import cint, formatdate, get_timestamp, today +from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today +from frappe.utils.background_jobs import get_job, is_job_enqueued from frappe.utils.nestedset import NestedSet, rebuild_tree from erpnext.accounts.doctype.account.account import get_account_currency @@ -900,8 +901,37 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad return None +def generate_id_for_deletion_job(company): + return "delete_company_transactions_" + company + + +@frappe.whitelist() +def is_deletion_job_running(company): + job_id = generate_id_for_deletion_job(company) + job_name = get_job(job_id).get_id() # job name will have site prefix + if is_job_enqueued(job_id): + frappe.throw( + _("A Transaction Deletion Job: {0} is already running for {1}").format( + frappe.bold(get_link_to_form("RQ Job", job_name)), frappe.bold(company) + ) + ) + + @frappe.whitelist() def create_transaction_deletion_request(company): + is_deletion_job_running(company) + job_id = generate_id_for_deletion_job(company) + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() - tdr.submit() + + frappe.enqueue( + "frappe.utils.background_jobs.run_doc_method", + doctype=tdr.doctype, + name=tdr.name, + doc_method="submit", + job_id=job_id, + queue="long", + enqueue_after_commit=True, + ) + frappe.msgprint(_("A Transaction Deletion Job is triggered for {0}").format(frappe.bold(company))) From 6148fb024b7157d637aa2308e7c856969858468d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jan 2024 17:08:30 +0530 Subject: [PATCH 25/57] test: dimension inheritance in PE reconciliation --- .../tests/test_accounts_controller.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index a448ad4a57..331599f787 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1342,7 +1342,7 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) - def test_51_cr_note_should_inherit_dimension_from_payment(self): + def test_51_cr_note_should_inherit_dimension(self): self.setup_dimensions() rate_in_account_currency = 1 @@ -1383,3 +1383,39 @@ class TestAccountsController(FrappeTestCase): [cr_note.department, cr_note.department], frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"), ) + + def test_52_dimension_inhertiance_exc_gain_loss(self): + # Sales Invoice in Foreign Currency + self.setup_dimensions() + rate = 80 + rate_in_account_currency = 1 + dimension = "Research & Development" + + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True) + si.department = dimension + si.save().submit() + + pe = self.create_payment_entry(amount=1, source_exc_rate=82).save() + pe.department = dimension + pe = pe.save().submit() + + pr = self.create_payment_reconciliation() + pr.department = dimension + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + journals = self.get_journals_for(si.doctype, si.name) + self.assertEqual( + [dimension, dimension], + frappe.db.get_all( + "Journal Entry Account", + filters={"parent": ("in", [x.parent for x in journals])}, + pluck="department", + ), + ) From cbd443a78afbc7c58055881e534a8aa56ca4bea6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jan 2024 16:44:20 +0530 Subject: [PATCH 26/57] refactor: pass dimensions on advance allocation --- erpnext/controllers/accounts_controller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1696df1cf0..d9aa7e8b5a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -28,6 +28,7 @@ from frappe.utils import ( import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, + get_dimensions, ) from erpnext.accounts.doctype.pricing_rule.utils import ( apply_pricing_rule_for_free_items, @@ -1423,7 +1424,13 @@ class AccountsController(TransactionBase): if lst: from erpnext.accounts.utils import reconcile_against_document - reconcile_against_document(lst) + # pass dimension values to utility method + active_dimensions = get_dimensions()[0] + for x in lst: + for dim in active_dimensions: + if self.get(dim.fieldname): + x.update({dim.fieldname: self.get(dim.fieldname)}) + reconcile_against_document(lst, active_dimensions=active_dimensions) def on_cancel(self): from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( From fcf4687c523202436234814af3da4c4d84f5eba9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jan 2024 16:50:54 +0530 Subject: [PATCH 27/57] test: dimension inheritance on adv allocation --- .../tests/test_accounts_controller.py | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 331599f787..fad216d5a4 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1389,18 +1389,18 @@ class TestAccountsController(FrappeTestCase): self.setup_dimensions() rate = 80 rate_in_account_currency = 1 - dimension = "Research & Development" + dpt = "Research & Development" si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True) - si.department = dimension + si.department = dpt si.save().submit() pe = self.create_payment_entry(amount=1, source_exc_rate=82).save() - pe.department = dimension + pe.department = dpt pe = pe.save().submit() pr = self.create_payment_reconciliation() - pr.department = dimension + pr.department = dpt pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) @@ -1410,9 +1410,57 @@ class TestAccountsController(FrappeTestCase): pr.reconcile() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 0) + + # Exc Gain/Loss journals should inherit dimension from parent journals = self.get_journals_for(si.doctype, si.name) self.assertEqual( - [dimension, dimension], + [dpt, dpt], + frappe.db.get_all( + "Journal Entry Account", + filters={"parent": ("in", [x.parent for x in journals])}, + pluck="department", + ), + ) + + def test_53_dimension_inheritance_on_advance(self): + self.setup_dimensions() + dpt = "Research & Development" + + adv = self.create_payment_entry(amount=1, source_exc_rate=85) + adv.department = dpt + adv.save().submit() + adv.reload() + + # Sales Invoices in different exchange rates + si = self.create_sales_invoice(qty=1, conversion_rate=82, rate=1, do_not_submit=True) + si.department = dpt + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "reference_row": advances[0].reference_row, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, + }, + ) + si = si.save().submit() + + # Outstanding in both currencies should be '0' + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exc Gain/Loss journals should inherit dimension from parent + journals = self.get_journals_for(si.doctype, si.name) + self.assertEqual( + [dpt, dpt], frappe.db.get_all( "Journal Entry Account", filters={"parent": ("in", [x.parent for x in journals])}, From b2d9380596dd8ca134777148337ef039f10cc2ca Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 19 Jan 2024 17:08:02 +0530 Subject: [PATCH 28/57] fix: party field in pdf html --- .../accounts_receivable/accounts_receivable.html | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index ed3b991559..7d8d33c46b 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -10,10 +10,8 @@

{%= __(report.report_name) %}

- {% if (filters.customer_name) { %} - {%= filters.customer_name %} - {% } else { %} - {%= filters.customer || filters.supplier %} + {% if (filters.party) { %} + {%= __(filters.party) %} {% } %}

@@ -141,7 +139,7 @@ {%= __("Reference") %} {% } %} {% if(!filters.show_future_payments) { %} - {%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %} + {%= (filters.party) ? __("Remarks"): __("Party") %} {% } %} {%= __("Invoiced Amount") %} {% if(!filters.show_future_payments) { %} @@ -158,7 +156,7 @@ {%= __("Remaining Balance") %} {% } %} {% } else { %} - {%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %} + {%= (filters.party) ? __("Remarks"): __("Party") %} {%= __("Total Invoiced Amount") %} {%= __("Total Paid Amount") %} {%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %} @@ -187,7 +185,7 @@ {% if(!filters.show_future_payments) { %} - {% if(!(filters.customer || filters.supplier)) { %} + {% if(!(filters.party)) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
{%= data[i]["customer_name"] %} @@ -260,7 +258,7 @@ {% if(data[i]["party"]|| " ") { %} {% if(!data[i]["is_total_row"]) { %} - {% if(!(filters.customer || filters.supplier)) { %} + {% if(!(filters.party)) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
{%= data[i]["customer_name"] %} From ebc8230d4564385b151e19f21d57172afe803108 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 21 Jan 2024 18:05:20 +0530 Subject: [PATCH 29/57] fix: key error during reposting --- erpnext/stock/stock_ledger.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a6206ac8dc..0370666263 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -439,7 +439,7 @@ def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None): reposting_data = get_reposting_data(doc.reposting_data_file) if reposting_data and reposting_data.distinct_item_and_warehouse: - return reposting_data.distinct_item_and_warehouse + return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse) distinct_item_warehouses = {} @@ -457,6 +457,16 @@ def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None): return distinct_item_warehouses +def parse_distinct_items_and_warehouses(distinct_items_and_warehouses): + new_dict = frappe._dict({}) + + # convert string keys to tuple + for k, v in distinct_items_and_warehouses.items(): + new_dict[frappe.safe_eval(k)] = frappe._dict(v) + + return new_dict + + def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]: if not reposting_data and doc and doc.reposting_data_file: reposting_data = get_reposting_data(doc.reposting_data_file) From b4393bc03d68769f57678bb56a09baf7b4ec3dc0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 21 Jan 2024 19:47:53 +0530 Subject: [PATCH 30/57] fix: added button to make serial / batch from Purchase Invoice --- .../purchase_invoice_item/purchase_invoice_item.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 9cf4e4fd7c..26984d96ef 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -64,6 +64,7 @@ "warehouse", "from_warehouse", "quality_inspection", + "add_serial_batch_bundle", "serial_and_batch_bundle", "serial_no", "col_br_wh", @@ -913,12 +914,18 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "depends_on": "eval:parent.update_stock === 1", + "fieldname": "add_serial_batch_bundle", + "fieldtype": "Button", + "label": "Add Serial / Batch No" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-12-25 22:00:28.043555", + "modified": "2024-01-21 19:46:25.537861", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", From 5d94f0bde55329411c419c242749e0260d3bb7c9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 21 Jan 2024 20:46:57 +0530 Subject: [PATCH 31/57] fix: UX improvements for Serial and Batch Bundle --- .../js/utils/serial_no_batch_selector.js | 71 ++++++++++++++++++- .../serial_and_batch_bundle.py | 16 +++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index bf362e338e..6c775f0db8 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -179,11 +179,52 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { label = __('Serial Nos / Batch Nos'); } - return [ + let fields = [ { fieldtype: 'Section Break', label: __('{0} {1} via CSV File', [primary_label, label]) - }, + } + ] + + if (this.item?.has_serial_no) { + fields = [...fields, + { + fieldtype: 'Check', + label: __('Upload Using CSV file'), + fieldname: 'upload_using_csv', + default: 0, + }, + { + fieldtype: 'Section Break', + depends_on: 'eval:doc.upload_using_csv === 0', + }, + { + fieldtype: 'Small Text', + label: __('Serial Nos'), + fieldname: 'upload_serial_nos', + depends_on: 'eval:doc.upload_using_csv === 0', + }, + { + fieldtype: 'Column Break', + depends_on: 'eval:doc.upload_using_csv === 0', + }, + { + fieldtype: 'Button', + fieldname: 'make_serial_nos', + label: __('Create Serial Nos'), + depends_on: 'eval:doc.upload_using_csv === 0', + click: () => { + this.create_serial_nos(); + } + }, + { + fieldtype: 'Section Break', + depends_on: 'eval:doc.upload_using_csv === 1', + } + ]; + } + + fields = [...fields, { fieldtype: 'Button', fieldname: 'download_csv', @@ -199,7 +240,31 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { label: __('Attach CSV File'), onchange: () => this.upload_csv_file() } - ] + ]; + + return fields; + } + + create_serial_nos() { + let {upload_serial_nos} = this.dialog.get_values(); + + if (!upload_serial_nos) { + frappe.throw(__('Please enter Serial Nos')); + } + + frappe.call({ + method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.create_serial_nos', + args: { + item_code: this.item.item_code, + serial_nos: upload_serial_nos + }, + callback: (r) => { + if (r.message) { + this.dialog.fields_dict.entries.df.data = []; + this.set_data(r.message); + } + } + }); } download_csv_file() { diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 2b87fcd175..856f1811e6 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -999,9 +999,25 @@ def get_serial_batch_from_data(item_code, kwargs): make_serial_nos(item_code, serial_nos) + if kwargs.get("_has_serial_nos"): + return serial_nos + return serial_nos, batch_nos +@frappe.whitelist() +def create_serial_nos(item_code, serial_nos): + serial_nos = get_serial_batch_from_data( + item_code, + { + "serial_nos": serial_nos, + "_has_serial_nos": True, + }, + ) + + return serial_nos + + def make_serial_nos(item_code, serial_nos): item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1) From 63ffce58cce6c68c210572287024da9ca414aca9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 21 Jan 2024 20:59:38 +0530 Subject: [PATCH 32/57] test: fixed test --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index aa5db57fa8..f6e9a07063 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -998,12 +998,6 @@ class TestWorkOrder(FrappeTestCase): make_job_card(wo_order.name, operations) job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name, "docstatus": 0}, "name") - update_job_card(job_card, 10, 2) - - stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) - for row in stock_entry.items: - if row.is_scrap_item: - self.assertEqual(row.qty, 2) def test_close_work_order(self): items = [ From f8bbb0619cbbbaace8f54a9f8758c3962ebe4725 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jan 2024 18:00:31 +0530 Subject: [PATCH 33/57] refactor: dynamic dimension filters in pop up --- .../doctype/payment_entry/payment_entry.js | 45 +++++++++---------- .../doctype/payment_entry/payment_entry.py | 8 ++++ .../public/js/utils/dimension_tree_filter.js | 4 ++ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index a98934a664..f5a07bf2ff 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -638,23 +638,9 @@ frappe.ui.form.on('Payment Entry', { frm.events.set_unallocated_amount(frm); }, - get_dimensions: function(frm) { - let result = []; - frappe.call({ - method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", - async: false, - callback: function(r) { - if(!r.exc) { - result = r.message[0].map(elem => elem.document_type); - } - } - }); - return result; - }, - get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { const today = frappe.datetime.get_today(); - const fields = [ + let fields = [ {fieldtype:"Section Break", label: __("Posting Date")}, {fieldtype:"Date", label: __("From Date"), fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)}, @@ -669,18 +655,29 @@ frappe.ui.form.on('Payment Entry', { fieldname:"outstanding_amt_greater_than", default: 0}, {fieldtype:"Column Break"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, - {fieldtype:"Section Break"}, - {fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center", - "get_query": function() { - return { - "filters": {"company": frm.doc.company} - } + ]; + + if (frm.dimension_filters) { + let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2); + + fields.push({fieldtype:"Section Break"}); + frm.dimension_filters.map((elem, idx)=>{ + fields.push({ + fieldtype: "Link", + label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label, + options: elem.document_type, + fieldname: elem.fieldname || elem.document_type + }); + if(idx+1 == column_break_insertion_point) { + fields.push({fieldtype:"Column Break"}); } - }, - {fieldtype:"Column Break"}, + }); + } + + fields = fields.concat([ {fieldtype:"Section Break"}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, - ]; + ]); let btn_text = ""; diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 37bd8e6fcd..45e0f0b5a0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -13,6 +13,7 @@ from pypika import Case from pypika.functions import Coalesce, Sum import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.bank_account.bank_account import ( get_bank_account_details, get_party_bank_account, @@ -1684,6 +1685,13 @@ def get_outstanding_reference_documents(args, validate=False): condition += " and cost_center='%s'" % args.get("cost_center") accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) + # dynamic dimension filters + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + if args.get(dim.fieldname): + condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname)) + accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname)) + date_fields_dict = { "posting_date": ["from_posting_date", "to_posting_date"], "due_date": ["from_due_date", "to_due_date"], diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 3f70c09f66..27d00bacb8 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -25,6 +25,10 @@ erpnext.accounts.dimensions = { }, setup_filters(frm, doctype) { + if (doctype == 'Payment Entry' && this.accounting_dimensions) { + frm.dimension_filters = this.accounting_dimensions + } + if (this.accounting_dimensions) { this.accounting_dimensions.forEach((dimension) => { frappe.model.with_doctype(dimension['document_type'], () => { From ec0f17ca8bd810e41ae73f5a45f304ba38c63d0a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jan 2024 11:59:20 +0530 Subject: [PATCH 34/57] refactor: update dimensions, only if provided --- erpnext/accounts/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index d688544122..5525af4545 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2097,7 +2097,8 @@ def create_gain_loss_journal( dr_or_cr + "_in_account_currency": 0, } ) - journal_account.update(dimensions) + if dimensions: + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_account = frappe._dict( @@ -2113,7 +2114,8 @@ def create_gain_loss_journal( reverse_dr_or_cr: abs(exc_gain_loss), } ) - journal_account.update(dimensions) + if dimensions: + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_entry.save() From 7c2cb70387d7dbb7f976d28919ce21f25a0b6acd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jan 2024 12:06:07 +0530 Subject: [PATCH 35/57] refactor: handle dynamic dimension in order query --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 45e0f0b5a0..76f9c4641d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1925,6 +1925,12 @@ def get_orders_to_be_billed( if doc and hasattr(doc, "cost_center") and doc.cost_center: condition = " and cost_center='%s'" % cost_center + # dynamic dimension filters + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + if filters.get(dim.fieldname): + condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname)) + if party_account_currency == company_currency: grand_total_field = "base_grand_total" rounded_total_field = "base_rounded_total" From fc0d2aeeffed9a2f87be0d87f0a0af0e837c5955 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Jan 2024 12:50:24 +0530 Subject: [PATCH 36/57] fix: auto create serial no on scan --- erpnext/public/js/utils/barcode_scanner.js | 93 ++++++++++++------- .../js/utils/serial_no_batch_selector.js | 43 +++++++-- .../serial_and_batch_bundle.js | 2 +- .../serial_and_batch_bundle.py | 29 ++++++ 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index cf7fab89ff..aacab0fe6c 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -105,32 +105,47 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.frm.has_items = false; } - if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) { - this.clean_up(); - reject(); - return; + if (serial_no) { + this.is_duplicate_serial_no(row, item_code, serial_no) + .then((is_duplicate) => { + if (!is_duplicate) { + this.run_serially_tasks(row, data, resolve); + } else { + this.clean_up(); + reject(); + return; + } + }); + } else { + this.run_serially_tasks(row, data, resolve); } - frappe.run_serially([ - () => this.set_serial_and_batch(row, item_code, serial_no, batch_no), - () => this.set_barcode(row, barcode), - () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { - this.show_scan_message(row.idx, row.item_code, qty); - }), - () => this.set_barcode_uom(row, uom), - () => this.clean_up(), - () => resolve(row), - () => { - if (row.serial_and_batch_bundle && !this.frm.is_new()) { - this.frm.save(); - } - frappe.flags.trigger_from_barcode_scanner = false; - } - ]); }); } + run_serially_tasks(row, data, resolve) { + const {item_code, barcode, batch_no, serial_no, uom} = data; + + frappe.run_serially([ + () => this.set_serial_and_batch(row, item_code, serial_no, batch_no), + () => this.set_barcode(row, barcode), + () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { + this.show_scan_message(row.idx, row.item_code, qty); + }), + () => this.set_barcode_uom(row, uom), + () => this.clean_up(), + () => { + if (row.serial_and_batch_bundle && !this.frm.is_new()) { + this.frm.save(); + } + + frappe.flags.trigger_from_barcode_scanner = false; + }, + () => resolve(row), + ]); + } + set_item(row, item_code, barcode, batch_no, serial_no) { return new Promise(resolve => { const increment = async (value = 1) => { @@ -475,26 +490,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - is_duplicate_serial_no(row, item_code, serial_no) { - if (this.frm.is_new() || !row.serial_and_batch_bundle) { - let is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no); - if (is_duplicate) { - this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); - } - - return is_duplicate; - } else if (row.serial_and_batch_bundle) { - this.check_duplicate_serial_no_in_db(row, serial_no, (r) => { - if (r.message) { + async is_duplicate_serial_no(row, item_code, serial_no) { + let is_duplicate = false; + const promise = new Promise((resolve, reject) => { + if (this.frm.is_new() || !row.serial_and_batch_bundle) { + is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no); + if (is_duplicate) { this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); } - return r.message; - }) - } + resolve(is_duplicate); + } else if (row.serial_and_batch_bundle) { + this.check_duplicate_serial_no_in_db(row, serial_no, (r) => { + if (r.message) { + this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange"); + } + + is_duplicate = r.message; + resolve(is_duplicate); + }) + } + }); + + return await promise; } - async check_duplicate_serial_no_in_db(row, serial_no, response) { + check_duplicate_serial_no_in_db(row, serial_no, response) { frappe.call({ method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no", args: { @@ -504,7 +525,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { callback(r) { response(r); } - }) + }); } check_duplicate_serial_no_in_localstorage(item_code, serial_no) { diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 6c775f0db8..44a4957b41 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -135,7 +135,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { filters: this.get_serial_no_filters() }; }, - onchange: () => this.update_serial_batch_no() + onchange: () => this.scan_barcode_data() }); } @@ -145,7 +145,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { options: 'Barcode', fieldname: 'scan_batch_no', label: __('Scan Batch No'), - onchange: () => this.update_serial_batch_no() + onchange: () => this.scan_barcode_data() }); } @@ -190,36 +190,38 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fields = [...fields, { fieldtype: 'Check', - label: __('Upload Using CSV file'), - fieldname: 'upload_using_csv', + label: __('Import Using CSV file'), + fieldname: 'import_using_csv_file', default: 0, }, { fieldtype: 'Section Break', - depends_on: 'eval:doc.upload_using_csv === 0', + label: __('{0} {1} Manually', [primary_label, label]), + depends_on: 'eval:doc.import_using_csv_file === 0', }, { fieldtype: 'Small Text', - label: __('Serial Nos'), + label: __('Enter Serial Nos'), fieldname: 'upload_serial_nos', - depends_on: 'eval:doc.upload_using_csv === 0', + depends_on: 'eval:doc.import_using_csv_file === 0', + description: __('Enter each serial no in a new line'), }, { fieldtype: 'Column Break', - depends_on: 'eval:doc.upload_using_csv === 0', + depends_on: 'eval:doc.import_using_csv_file === 0', }, { fieldtype: 'Button', fieldname: 'make_serial_nos', label: __('Create Serial Nos'), - depends_on: 'eval:doc.upload_using_csv === 0', + depends_on: 'eval:doc.import_using_csv_file === 0', click: () => { this.create_serial_nos(); } }, { fieldtype: 'Section Break', - depends_on: 'eval:doc.upload_using_csv === 1', + depends_on: 'eval:doc.import_using_csv_file === 1', } ]; } @@ -262,6 +264,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { if (r.message) { this.dialog.fields_dict.entries.df.data = []; this.set_data(r.message); + this.update_bundle_entries(); } } }); @@ -439,6 +442,26 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } } + scan_barcode_data() { + const { scan_serial_no, scan_batch_no } = this.dialog.get_values(); + + if (scan_serial_no || scan_batch_no) { + frappe.call({ + method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists', + args: { + item_code: this.item.item_code, + type_of_transaction: this.item.type_of_transaction, + serial_no: scan_serial_no, + batch_no: scan_batch_no, + }, + callback: (r) => { + this.update_serial_batch_no(); + } + + }) + } + } + update_serial_batch_no() { const { scan_serial_no, scan_batch_no } = this.dialog.get_values(); diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index 9f01ee9ae6..91b743016b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -74,7 +74,7 @@ frappe.ui.form.on('Serial and Batch Bundle', { let fields = [ { - "label": __("Using CSV File"), + "label": __("Import Using CSV file"), "fieldname": "using_csv_file", "default": 1, "fieldtype": "Check", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 856f1811e6..63cc938c09 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2095,6 +2095,35 @@ def get_batch_no_from_serial_no(serial_no): return frappe.get_cached_value("Serial No", serial_no, "batch_no") +@frappe.whitelist() +def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, batch_no=None): + if serial_no and not frappe.db.exists("Serial No", serial_no): + if type_of_transaction != "Inward": + frappe.throw(_("Serial No {0} does not exists").format(serial_no)) + + make_serial_no(serial_no, item_code) + + if batch_no and frappe.db.exists("Batch", batch_no): + if type_of_transaction != "Inward": + frappe.throw(_("Batch No {0} does not exists").format(batch_no)) + + make_batch_no(batch_no, item_code) + + +def make_serial_no(serial_no, item_code): + serial_no_doc = frappe.new_doc("Serial No") + serial_no_doc.serial_no = serial_no + serial_no_doc.item_code = item_code + serial_no_doc.save(ignore_permissions=True) + + +def make_batch_no(batch_no, item_code): + batch_doc = frappe.new_doc("Batch") + batch_doc.batch_id = batch_no + batch_doc.item = item_code + batch_doc.save(ignore_permissions=True) + + @frappe.whitelist() def is_duplicate_serial_no(bundle_id, serial_no): return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no}) From 52814724eb3e7d0a350ddeb01214ba3855a1ccc9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jan 2024 14:43:15 +0530 Subject: [PATCH 37/57] refactor: move 'project' set_query to sales_common.js --- erpnext/public/js/utils/sales_common.js | 11 ++++++++++- erpnext/selling/doctype/sales_order/sales_order.js | 9 --------- erpnext/stock/doctype/delivery_note/delivery_note.js | 9 --------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index b92b02e826..b8ec77f8e5 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -22,6 +22,15 @@ erpnext.sales_common = { } }; }); + + this.frm.set_query('project', function(doc) { + return { + query: "erpnext.controllers.queries.get_project_name", + filters: { + 'customer': doc.customer + } + } + }); } setup_queries() { @@ -439,4 +448,4 @@ erpnext.pre_sales = { } }); } -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 56c745c00a..2bb093dbaf 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -144,15 +144,6 @@ frappe.ui.form.on("Sales Order", { }; }); - frm.set_query('project', function(doc, cdt, cdn) { - return { - query: "erpnext.controllers.queries.get_project_name", - filters: { - 'customer': doc.customer - } - } - }); - frm.set_query('warehouse', 'items', function(doc, cdt, cdn) { let row = locals[cdt][cdn]; let query = { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index ec68549846..14aedca39e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -31,15 +31,6 @@ frappe.ui.form.on("Delivery Note", { }); erpnext.queries.setup_warehouse_query(frm); - frm.set_query('project', function(doc) { - return { - query: "erpnext.controllers.queries.get_project_name", - filters: { - 'customer': doc.customer - } - } - }) - frm.set_query('transporter', function() { return { filters: { From aaf83da3e9a28244c0f57f01a86b98257bdf503b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Jan 2024 16:43:54 +0530 Subject: [PATCH 38/57] fix: UOM needs to be whole number not being checked in quotations --- erpnext/selling/doctype/quotation/quotation.py | 3 ++- .../selling/doctype/quotation/test_quotation.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ab74f7f738..654f2978fe 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -127,7 +127,8 @@ class Quotation(SellingController): def validate(self): super(Quotation, self).validate() self.set_status() - self.validate_uom_is_integer("stock_uom", "qty") + self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_uom_is_integer("uom", "qty") self.validate_valid_till() self.set_customer_name() if self.items: diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index ecb7d097b8..2a4855e318 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -593,6 +593,22 @@ class TestQuotation(FrappeTestCase): quotation.reload() self.assertEqual(quotation.status, "Ordered") + def test_uom_validation(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = "_Test Item FOR UOM Validation" + make_item(item, {"is_stock_item": 1}) + + if not frappe.db.exists("UOM", "lbs"): + frappe.get_doc({"doctype": "UOM", "uom_name": "lbs", "must_be_whole_number": 1}).insert() + else: + frappe.db.set_value("UOM", "lbs", "must_be_whole_number", 1) + + quotation = make_quotation(item_code=item, qty=1, rate=100, do_not_submit=1) + quotation.items[0].uom = "lbs" + quotation.items[0].conversion_factor = 2.23 + self.assertRaises(frappe.ValidationError, quotation.save) + test_records = frappe.get_test_records("Quotation") From 236b73565e6889aaa7027929f770918e89bca7fc Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 22 Jan 2024 21:13:32 +0530 Subject: [PATCH 39/57] fix: skip liability account for internal transfer --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index dbebbb00fa..af6047bd27 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -189,7 +189,7 @@ class PaymentEntry(AccountsController): def set_liability_account(self): # Auto setting liability account should only be done during 'draft' status - if self.docstatus > 0: + if self.docstatus > 0 or self.payment_type == "Internal Transfer": return if not frappe.db.get_value( From 31592b8f3ac16298d7c5f58c9d83bc764a3ea51b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 23 Jan 2024 12:42:54 +0530 Subject: [PATCH 40/57] fix: cancellation of asset/asset capitalization --- erpnext/assets/doctype/asset/asset.py | 13 +++++-------- erpnext/assets/doctype/asset/depreciation.py | 2 ++ .../asset_capitalization/asset_capitalization.py | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 73572499f2..67018106a7 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -519,14 +519,11 @@ class Asset(AccountsController): movement.cancel() def cancel_capitalization(self): - asset_capitalization = frappe.db.get_value( - "Asset Capitalization", - {"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"}, - ) - - if asset_capitalization: - asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization) - asset_capitalization.cancel() + if self.capitalized_in: + self.capitalized_in = None + asset_capitalization = frappe.get_doc("Asset Capitalization", self.capitalized_in) + if asset_capitalization.docstatus == 1: + asset_capitalization.cancel() def delete_depreciation_entries(self): if self.calculate_depreciation: diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a93af94664..df4593bb69 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -561,6 +561,8 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes): def reverse_depreciation_entry_made_after_disposal(asset, date): for row in asset.get("finance_books"): asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) + if not asset_depr_schedule_doc: + continue for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): if schedule.schedule_date == date: diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index cad74df51e..5e251a5658 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -146,6 +146,7 @@ class AssetCapitalization(StockController): def cancel_target_asset(self): if self.entry_type == "Capitalization" and self.target_asset: asset_doc = frappe.get_doc("Asset", self.target_asset) + frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None) if asset_doc.docstatus == 1: asset_doc.cancel() From 1a686cb66d92d595a64ed1dd41a96f579e0c6e8a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 23 Jan 2024 12:46:15 +0530 Subject: [PATCH 41/57] fix: Use db_set to set a value in on_cancel --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 67018106a7..259857f554 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -520,7 +520,7 @@ class Asset(AccountsController): def cancel_capitalization(self): if self.capitalized_in: - self.capitalized_in = None + self.db_set("capitalized_in", None) asset_capitalization = frappe.get_doc("Asset Capitalization", self.capitalized_in) if asset_capitalization.docstatus == 1: asset_capitalization.cancel() From 1a670ff266c83ea1b18ede1c504873c08c310de1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 23 Jan 2024 13:27:37 +0530 Subject: [PATCH 42/57] fix: Serial No Ledger permission issue --- erpnext/stock/report/serial_no_ledger/serial_no_ledger.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index 810dc4666f..3f5216bae8 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -22,9 +22,8 @@ def get_columns(filters): {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90}, { "label": _("Voucher Type"), - "fieldtype": "Link", + "fieldtype": "Data", "fieldname": "voucher_type", - "options": "DocType", "width": 160, }, { From c88ce552425f077f59e98458799819ff3dd72742 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 22 Jan 2024 22:04:30 +0100 Subject: [PATCH 43/57] fix: advance payment doctypes to keep input/output distinction --- .../accounts/doctype/journal_entry/journal_entry.py | 5 ++++- .../accounts/doctype/payment_entry/payment_entry.py | 10 ++++++++-- erpnext/accounts/utils.py | 10 ++++++++-- erpnext/controllers/accounts_controller.py | 5 ++++- erpnext/hooks.py | 3 ++- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 40d552bc88..7579da86cd 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -186,9 +186,12 @@ class JournalEntry(AccountsController): def update_advance_paid(self): advance_paid = frappe._dict() + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_customer_doctypes" + ) + frappe.get_hooks("advance_payment_supplier_doctypes") for d in self.get("accounts"): if d.is_advance: - if d.reference_type in frappe.get_hooks("advance_payment_doctypes"): + if d.reference_type in advance_payment_doctypes: advance_paid.setdefault(d.reference_type, []).append(d.reference_name) for voucher_type, order_list in advance_paid.items(): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index dbebbb00fa..b8781ef120 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -925,7 +925,10 @@ class PaymentEntry(AccountsController): def calculate_base_allocated_amount_for_reference(self, d) -> float: base_allocated_amount = 0 - if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_customer_doctypes" + ) + frappe.get_hooks("advance_payment_supplier_doctypes") + if d.reference_doctype in advance_payment_doctypes: # When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type. # This is so there are no Exchange Gain/Loss generated for such doctypes @@ -1423,8 +1426,11 @@ class PaymentEntry(AccountsController): def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_customer_doctypes" + ) + frappe.get_hooks("advance_payment_supplier_doctypes") for d in self.get("references"): - if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): + if d.allocated_amount and d.reference_doctype in advance_payment_doctypes: frappe.get_doc( d.reference_doctype, d.reference_name, for_update=True ).set_total_advance_paid() diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 19095bc46e..9b70629994 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -600,7 +600,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] # Update Advance Paid in SO/PO since they might be getting unlinked - if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"): + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_customer_doctypes" + ) + frappe.get_hooks("advance_payment_supplier_doctypes") + if jv_detail.get("reference_type") in advance_payment_doctypes: frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid() if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: @@ -673,7 +676,10 @@ def update_reference_in_payment_entry( existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] # Update Advance Paid in SO/PO since they are getting unlinked - if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"): + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_customer_doctypes" + ) + frappe.get_hooks("advance_payment_supplier_doctypes") + if existing_row.get("reference_doctype") in advance_payment_doctypes: frappe.get_doc( existing_row.reference_doctype, existing_row.reference_name ).set_total_advance_paid() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8848a3c385..ed0c1d7383 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1749,7 +1749,10 @@ class AccountsController(TransactionBase): def set_total_advance_paid(self): ple = frappe.qb.DocType("Payment Ledger Entry") - party = self.customer if self.doctype == "Sales Order" else self.supplier + if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"): + party = self.customer + if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"): + party = self.supplier advance = ( frappe.qb.from_(ple) .select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount")) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6efb893e63..e21d7bd79b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -481,7 +481,8 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account communication_doctypes = ["Customer", "Supplier"] -advance_payment_doctypes = ["Sales Order", "Purchase Order"] +advance_payment_customer_doctypes = ["Sales Order"] +advance_payment_supplier_doctypes = ["Purchase Order"] invoice_doctypes = ["Sales Invoice", "Purchase Invoice"] From b1aef01a1ff76fcdcf226a76bb64eb2a9b5b6e01 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 22 Jan 2024 22:15:49 +0100 Subject: [PATCH 44/57] fix: always update the advance payment status --- .../payment_request/payment_request.py | 59 ++++--------------- erpnext/controllers/accounts_controller.py | 39 ++++++++++-- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 1e8cfea9e8..839348a541 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -169,20 +169,12 @@ class PaymentRequest(Document): elif self.payment_channel == "Phone": self.request_phone_payment() - if ( - self.reference_doctype in ["Sales Order"] and ref_doc.advance_payment_status == "Not Requested" - ): - ref_doc.db_set("advance_payment_status", "Requested") - ref_doc.set_status(update=True) - ref_doc.notify_update() - - if ( - self.reference_doctype in ["Purchase Order"] - and ref_doc.advance_payment_status == "Not Initiated" - ): - ref_doc.db_set("advance_payment_status", "Initiated") - ref_doc.set_status(update=True) - ref_doc.notify_update() + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_customer_doctypes" + ) + frappe.get_hooks("advance_payment_supplier_doctypes") + if self.reference_doctype in advance_payment_doctypes: + # set advance payment status + ref_doc.set_total_advance_paid() def request_phone_payment(self): controller = _get_payment_gateway_controller(self.payment_gateway) @@ -222,38 +214,13 @@ class PaymentRequest(Document): self.check_if_payment_entry_exists() self.set_as_cancelled() - if self.reference_doctype in ["Sales Order", "Purchase Order"]: - - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if self.reference_doctype in ["Sales Order"] and ref_doc.advance_payment_status == "Requested": - peer_pr = frappe.db.count( - "Payment Request", - { - "reference_doctype": self.reference_doctype, - "reference_name": self.reference_name, - "docstatus": 1, - }, - ) - if not peer_pr: - ref_doc.db_set("advance_payment_status", "Not Requested") - ref_doc.set_status(update=True) - ref_doc.notify_update() - - if ( - self.reference_doctype in ["Purchase Order"] and ref_doc.advance_payment_status == "Initiated" - ): - peer_pr = frappe.db.count( - "Payment Request", - { - "reference_doctype": self.reference_doctype, - "reference_name": self.reference_name, - "docstatus": 1, - }, - ) - if not peer_pr: - ref_doc.db_set("advance_payment_status", "Not Initiated") - ref_doc.set_status(update=True) - ref_doc.notify_update() + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_customer_doctypes" + ) + frappe.get_hooks("advance_payment_supplier_doctypes") + if self.reference_doctype in advance_payment_doctypes: + # set advance payment status + ref_doc.set_total_advance_paid() def make_invoice(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ed0c1d7383..159e217e4b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1766,6 +1766,8 @@ class AccountsController(TransactionBase): .run(as_dict=True) ) + advance_paid, order_total = None, None + if advance: advance = advance[0] @@ -1794,13 +1796,38 @@ class AccountsController(TransactionBase): ).format(formatted_advance_paid, self.name, formatted_order_total) ) - frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid) - frappe.db.set_value( - self.doctype, - self.name, - "advance_payment_status", - "Partially Paid" if advance_paid < order_total else "Paid", + self.db_set("advance_paid", advance_paid) + + self.set_advance_payment_status(advance_paid, order_total) + + def set_advance_payment_status( + self, advance_paid: float | None = None, order_total: float | None = None + ): + new_status = None + # if money is paid set the paid states + if advance_paid: + new_status = "Partially Paid" if advance_paid < order_total else "Paid" + + if not new_status: + prs = frappe.db.count( + "Payment Request", + { + "reference_doctype": self.doctype, + "reference_name": self.name, + "docstatus": 1, + }, ) + if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"): + new_status = "Requested" if prs else "Not Requested" + if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"): + new_status = "Initiated" if prs else "Not Initiated" + + if new_status == self.advance_payment_status: + return + + self.db_set("advance_payment_status", new_status) + self.set_status(update=True) + self.notify_update() @property def company_abbr(self): From 7d8aa469d78560fb5f11f7370db74c517d0d0c00 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 22 Jan 2024 22:46:03 +0100 Subject: [PATCH 45/57] fix: align with 'Partly/Fully X' nomenclature in so & po --- erpnext/buying/doctype/purchase_order/purchase_order.json | 2 +- erpnext/controllers/accounts_controller.py | 2 +- erpnext/patches/v15_0/create_advance_payment_status.py | 4 ++-- erpnext/selling/doctype/sales_order/sales_order.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 97f2310c1a..9da49a79ee 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1280,7 +1280,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Not Initiated\nInitiated\nPartially Paid\nPaid", + "options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid", "print_hide": 1 } ], diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 159e217e4b..7cc4bfe2c9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1806,7 +1806,7 @@ class AccountsController(TransactionBase): new_status = None # if money is paid set the paid states if advance_paid: - new_status = "Partially Paid" if advance_paid < order_total else "Paid" + new_status = "Partially Paid" if advance_paid < order_total else "Fully Paid" if not new_status: prs = frappe.db.count( diff --git a/erpnext/patches/v15_0/create_advance_payment_status.py b/erpnext/patches/v15_0/create_advance_payment_status.py index ff5ba8f2d5..18ab9fa88c 100644 --- a/erpnext/patches/v15_0/create_advance_payment_status.py +++ b/erpnext/patches/v15_0/create_advance_payment_status.py @@ -19,7 +19,7 @@ def execute(): so.advance_paid < (so.rounded_total or so.grand_total) ).run() - frappe.qb.update(so).set(so.advance_payment_status, "Paid").where(so.docstatus == 1).where( + frappe.qb.update(so).set(so.advance_payment_status, "Fully Paid").where(so.docstatus == 1).where( so.advance_payment_status.isnull() ).where(so.advance_paid == (so.rounded_total or so.grand_total)).run() @@ -42,7 +42,7 @@ def execute(): po.advance_paid < (po.rounded_total or po.grand_total) ).run() - frappe.qb.update(po).set(po.advance_payment_status, "Paid").where(po.docstatus == 1).where( + frappe.qb.update(po).set(po.advance_payment_status, "Fully Paid").where(po.docstatus == 1).where( po.advance_payment_status.isnull() ).where(po.advance_paid == (po.rounded_total or po.grand_total)).run() diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 5b80dfd4d4..3c516d0ea3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1649,7 +1649,7 @@ "in_standard_filter": 1, "label": "Advance Payment Status", "no_copy": 1, - "options": "Not Requested\nRequested\nPartially Paid\nPaid", + "options": "Not Requested\nRequested\nPartially Paid\nFully Paid", "print_hide": 1 } ], From bd6a4ca1d7f419f8b0c766c5b7aae0f3513ebc22 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 22 Jan 2024 22:54:36 +0100 Subject: [PATCH 46/57] fix: move value initialization to ensure it is commited --- erpnext/buying/doctype/purchase_order/purchase_order.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index fcdc87de1c..4efbb270e9 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -215,6 +215,10 @@ class PurchaseOrder(BuyingController): self.validate_fg_item_for_subcontracting() self.set_received_qty_for_drop_ship_items() + + if not self.advance_payment_status: + self.advance_payment_status = "Not Initiated" + validate_inter_company_party( self.doctype, self.supplier, self.company, self.inter_company_order_reference ) @@ -470,9 +474,6 @@ class PurchaseOrder(BuyingController): self.validate_budget() self.update_reserved_qty_for_subcontract() - if not self.advance_payment_status: - self.advance_payment_status = "Not Initiated" - frappe.get_doc("Authorization Control").validate_approving_authority( self.doctype, self.company, self.base_grand_total ) From 8de03ef83698b1d619c158d0c5215c4d3a0584a4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 23 Jan 2024 15:50:52 +0530 Subject: [PATCH 47/57] test: advance payment status for Sales Order --- .../doctype/sales_order/test_sales_order.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ac7fdb1b45..5ae48ee561 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1996,6 +1996,33 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) + def test_sales_order_advance_payment_status(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request + + so = make_sales_order(qty=1, rate=100) + self.assertEqual( + frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested" + ) + + pr = make_payment_request(dt=so.doctype, dn=so.name, submit_doc=True, return_doc=True) + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested") + + pe = get_payment_entry(so.doctype, so.name).save().submit() + self.assertEqual( + frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid" + ) + + pe.reload() + pe.cancel() + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested") + + pr.reload() + pr.cancel() + self.assertEqual( + frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested" + ) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From 9b4e757b0b0608a90c90d8d77916bd7ba4389095 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 23 Jan 2024 15:51:09 +0530 Subject: [PATCH 48/57] test: advance payment status for Purchase Order --- .../purchase_order/test_purchase_order.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 9b382bbd7e..5405799b4e 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1021,6 +1021,33 @@ class TestPurchaseOrder(FrappeTestCase): self.assertTrue(frappe.db.get_value("Subcontracting Order", {"purchase_order": po.name})) + def test_purchase_order_advance_payment_status(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request + + po = create_purchase_order() + self.assertEqual( + frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated" + ) + + pr = make_payment_request(dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True) + self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated") + + pe = get_payment_entry(po.doctype, po.name).save().submit() + self.assertEqual( + frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid" + ) + + pe.reload() + pe.cancel() + self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated") + + pr.reload() + pr.cancel() + self.assertEqual( + frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated" + ) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 3c7e7a76f0c89b6bedf0b27062895f05f68c1fa5 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:23:32 +0100 Subject: [PATCH 49/57] refactor: split batch --- erpnext/stock/doctype/batch/batch.py | 62 ++++++++++++++-------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 7b23f9ec01..e8e94fda31 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -9,7 +9,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last from frappe.query_builder.functions import CurDate, Sum -from frappe.utils import cint, flt, get_link_to_form, nowtime, today +from frappe.utils import cint, flt, get_link_to_form from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -248,8 +248,9 @@ def get_batches_by_oldest(item_code, warehouse): @frappe.whitelist() -def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): - +def split_batch( + batch_no: str, item_code: str, warehouse: str, qty: float, new_batch_id: str | None = None +): """Split the batch into a new batch""" batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert() qty = flt(qty) @@ -257,29 +258,21 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): company = frappe.db.get_value("Warehouse", warehouse, "company") from_bundle_id = make_batch_bundle( - frappe._dict( - { - "item_code": item_code, - "warehouse": warehouse, - "batches": frappe._dict({batch_no: qty}), - "company": company, - "type_of_transaction": "Outward", - "qty": qty, - } - ) + item_code=item_code, + warehouse=warehouse, + batches=frappe._dict({batch_no: qty}), + company=company, + type_of_transaction="Outward", + qty=qty, ) to_bundle_id = make_batch_bundle( - frappe._dict( - { - "item_code": item_code, - "warehouse": warehouse, - "batches": frappe._dict({batch.name: qty}), - "company": company, - "type_of_transaction": "Inward", - "qty": qty, - } - ) + item_code=item_code, + warehouse=warehouse, + batches=frappe._dict({batch.name: qty}), + company=company, + type_of_transaction="Inward", + qty=qty, ) stock_entry = frappe.get_doc( @@ -304,21 +297,30 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): return batch.name -def make_batch_bundle(kwargs): +def make_batch_bundle( + item_code: str, + warehouse: str, + batches: dict[str, float], + company: str, + type_of_transaction: str, + qty: float, +): + from frappe.utils import nowtime, today + from erpnext.stock.serial_batch_bundle import SerialBatchCreation return ( SerialBatchCreation( { - "item_code": kwargs.item_code, - "warehouse": kwargs.warehouse, + "item_code": item_code, + "warehouse": warehouse, "posting_date": today(), "posting_time": nowtime(), "voucher_type": "Stock Entry", - "qty": flt(kwargs.qty), - "type_of_transaction": kwargs.type_of_transaction, - "company": kwargs.company, - "batches": kwargs.batches, + "qty": qty, + "type_of_transaction": type_of_transaction, + "company": company, + "batches": batches, "do_not_submit": True, } ) From 34ec2f8a2b805e63c531ad293f3adb2061c219eb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:57:23 +0100 Subject: [PATCH 50/57] fix(Batch): reload doc after splitting to show updated qty --- erpnext/stock/doctype/batch/batch.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js index 7bf7a1f65d..2d12a27d60 100644 --- a/erpnext/stock/doctype/batch/batch.js +++ b/erpnext/stock/doctype/batch/batch.js @@ -128,19 +128,16 @@ frappe.ui.form.on('Batch', { fieldtype: 'Data', }], (data) => { - frappe.call({ - method: 'erpnext.stock.doctype.batch.batch.split_batch', - args: { + frappe.xcall( + 'erpnext.stock.doctype.batch.batch.split_batch', + { item_code: frm.doc.item, batch_no: frm.doc.name, qty: data.qty, warehouse: $btn.attr('data-warehouse'), new_batch_id: data.new_batch_id - }, - callback: (r) => { - frm.refresh(); - }, - }); + } + ).then(() => frm.reload_doc()); }, __('Split Batch'), __('Split') From 7a7a2132855e496a0b1d51698cdb7d675c527650 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:02:42 +0100 Subject: [PATCH 51/57] refactor(Batch): use const instead of var --- erpnext/stock/doctype/batch/batch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js index 2d12a27d60..f4a935aa90 100644 --- a/erpnext/stock/doctype/batch/batch.js +++ b/erpnext/stock/doctype/batch/batch.js @@ -52,7 +52,7 @@ frappe.ui.form.on('Batch', { // sort by qty r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 }); - var rows = $('
').appendTo(section); + const rows = $('
').appendTo(section); // show (r.message || []).forEach(function(d) { @@ -76,7 +76,7 @@ frappe.ui.form.on('Batch', { // move - ask for target warehouse and make stock entry rows.find('.btn-move').on('click', function() { - var $btn = $(this); + const $btn = $(this); const fields = [ { fieldname: 'to_warehouse', @@ -115,7 +115,7 @@ frappe.ui.form.on('Batch', { // split - ask for new qty and batch ID (optional) // and make stock entry via batch.batch_split rows.find('.btn-split').on('click', function() { - var $btn = $(this); + const $btn = $(this); frappe.prompt([{ fieldname: 'qty', label: __('New Batch Qty'), From 806696a0032e3c2e9a6b677d5fde36528dfbecc7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 24 Jan 2024 02:59:52 +0100 Subject: [PATCH 52/57] fix: typos --- .../in_standard_chart_of_accounts.json | 8 ++++---- .../verified/standard_chart_of_accounts.py | 10 +++++----- ...d_chart_of_accounts_with_account_number.py | 10 +++++----- .../accounts/doctype/account/test_account.py | 2 +- .../doctype/bank_account/bank_account.py | 2 +- .../doctype/bank_guarantee/bank_guarantee.py | 6 +++--- .../doctype/coupon_code/coupon_code.json | 4 ++-- .../invoice_discounting.js | 2 +- .../opening_invoice_creation_tool.py | 2 +- .../doctype/pos_invoice/pos_invoice.py | 4 ++-- .../doctype/pos_profile/pos_profile.py | 2 +- .../doctype/pricing_rule/pricing_rule.json | 4 ++-- .../doctype/pricing_rule/pricing_rule.py | 2 +- .../promotional_scheme.json | 4 ++-- .../doctype/subscription/subscription.json | 4 ++-- .../doctype/subscription/subscription.py | 6 ++---- .../doctype/subscription/subscription_list.js | 4 ++-- .../doctype/subscription/test_subscription.py | 2 +- .../accounts_settings/accounts_settings.json | 4 ++-- .../report/account_balance/account_balance.js | 2 +- erpnext/assets/doctype/asset/asset.py | 2 +- .../asset_maintenance/asset_maintenance.py | 2 +- .../asset_category/asset_category.json | 4 ++-- .../fixed_asset_register.py | 2 +- .../controllers/sales_and_purchase_return.py | 2 +- .../tally_migration/tally_migration.py | 2 +- .../maintenance_schedule.py | 2 +- .../doctype/work_order/work_order.py | 2 +- .../v14_0/migrate_gl_to_payment_ledger.py | 2 +- .../lower_deduction_certificate.json | 4 ++-- .../lower_deduction_certificate.py | 4 ++-- .../selling/doctype/customer/test_customer.py | 6 +++--- .../doctype/sales_order/sales_order.py | 2 +- .../page/point_of_sale/pos_controller.js | 2 +- .../authorization_control.py | 2 +- erpnext/setup/doctype/employee/employee.json | 8 ++++---- .../doctype/delivery_note/delivery_note.py | 20 +++++++++---------- .../stock/doctype/item_price/item_price.json | 4 ++-- .../stock/doctype/item_price/item_price.py | 2 +- .../doctype/item_price/test_item_price.py | 2 +- .../material_request/material_request.py | 2 +- .../material_request/material_request_list.js | 2 +- .../purchase_receipt/purchase_receipt.py | 16 +++++++-------- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../stock_settings/stock_settings.json | 4 ++-- erpnext/stock/reorder_item.py | 2 +- erpnext/utilities/activation.py | 2 +- 47 files changed, 94 insertions(+), 96 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json index 2ec0b7f70c..56b22a63d8 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json @@ -36,16 +36,16 @@ } }, "Fixed Assets": { - "Capital Equipments": { + "Capital Equipment": { "account_type": "Fixed Asset" }, - "Electronic Equipments": { + "Electronic Equipment": { "account_type": "Fixed Asset" }, - "Furnitures and Fixtures": { + "Furniture and Fixtures": { "account_type": "Fixed Asset" }, - "Office Equipments": { + "Office Equipment": { "account_type": "Fixed Asset" }, "Plants and Machineries": { diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py index e30ad24a37..0699932360 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py @@ -23,13 +23,13 @@ def get(): _("Tax Assets"): {"is_group": 1}, }, _("Fixed Assets"): { - _("Capital Equipments"): {"account_type": "Fixed Asset"}, - _("Electronic Equipments"): {"account_type": "Fixed Asset"}, - _("Furnitures and Fixtures"): {"account_type": "Fixed Asset"}, - _("Office Equipments"): {"account_type": "Fixed Asset"}, + _("Capital Equipment"): {"account_type": "Fixed Asset"}, + _("Electronic Equipment"): {"account_type": "Fixed Asset"}, + _("Furniture and Fixtures"): {"account_type": "Fixed Asset"}, + _("Office Equipment"): {"account_type": "Fixed Asset"}, _("Plants and Machineries"): {"account_type": "Fixed Asset"}, _("Buildings"): {"account_type": "Fixed Asset"}, - _("Softwares"): {"account_type": "Fixed Asset"}, + _("Software"): {"account_type": "Fixed Asset"}, _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"}, _("CWIP Account"): { "account_type": "Capital Work in Progress", diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py index 0e46f1e08a..ee4da732e7 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py @@ -36,13 +36,13 @@ def get(): "account_number": "1100-1600", }, _("Fixed Assets"): { - _("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"}, - _("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"}, - _("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"}, - _("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"}, + _("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"}, + _("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"}, + _("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"}, + _("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"}, _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"}, _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"}, - _("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"}, + _("Software"): {"account_type": "Fixed Asset", "account_number": "1770"}, _("Accumulated Depreciation"): { "account_type": "Accumulated Depreciation", "account_number": "1780", diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 30eebef7fb..eb3e00b388 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -119,7 +119,7 @@ class TestAccount(unittest.TestCase): InvalidAccountMergeError, merge_account, "Capital Stock - _TC", - "Softwares - _TC", + "Software - _TC", ) # Raise error as currency doesn't match diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 4b99b198de..ace4bb193d 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -55,7 +55,7 @@ class BankAccount(Document): def validate_company(self): if self.is_company_account and not self.company: - frappe.throw(_("Company is manadatory for company account")) + frappe.throw(_("Company is mandatory for company account")) def validate_iban(self): """ diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index 0af2caf111..4326c404fd 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -48,11 +48,11 @@ class BankGuarantee(Document): def on_submit(self): if not self.bank_guarantee_number: - frappe.throw(_("Enter the Bank Guarantee Number before submittting.")) + frappe.throw(_("Enter the Bank Guarantee Number before submitting.")) if not self.name_of_beneficiary: - frappe.throw(_("Enter the name of the Beneficiary before submittting.")) + frappe.throw(_("Enter the name of the Beneficiary before submitting.")) if not self.bank: - frappe.throw(_("Enter the name of the bank or lending institution before submittting.")) + frappe.throw(_("Enter the name of the bank or lending institution before submitting.")) @frappe.whitelist() diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.json b/erpnext/accounts/doctype/coupon_code/coupon_code.json index 7dc5e9dc78..c6b1477a6e 100644 --- a/erpnext/accounts/doctype/coupon_code/coupon_code.json +++ b/erpnext/accounts/doctype/coupon_code/coupon_code.json @@ -80,7 +80,7 @@ { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Up To" }, { "depends_on": "eval: doc.coupon_type == \"Promotional\"", @@ -115,7 +115,7 @@ "read_only": 1 } ], - "modified": "2019-10-19 14:48:14.602481", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Accounts", "name": "Coupon Code", diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js index db4f7c423f..c80bf6243b 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.js @@ -154,7 +154,7 @@ frappe.ui.form.on('Invoice Discounting', { } }); }, - primary_action_label: __('Get Invocies') + primary_action_label: __('Get Invoices') }); d.show(); }, diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index f5f8f8ab10..acd993376a 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -270,7 +270,7 @@ def start_import(invoices): errors, "Error Log" ), indicator="red", - title=_("Error Occured"), + title=_("Error Occurred"), ) return names diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e542d3cc63..ca031f0e6b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -371,7 +371,7 @@ class POSInvoice(SalesInvoice): if d.get("qty") > 0: frappe.throw( _( - "Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return." + "Row #{}: You cannot add positive quantities in a return invoice. Please remove item {} to complete the return." ).format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"), ) @@ -793,7 +793,7 @@ def make_merge_log(invoices): invoices = json.loads(invoices) if len(invoices) == 0: - frappe.throw(_("Atleast one invoice has to be selected.")) + frappe.throw(_("At least one invoice has to be selected.")) merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log.posting_date = getdate(nowdate()) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 30f3e0cc9e..c1add57bc5 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -132,7 +132,7 @@ class POSProfile(Document): if len(customer_groups) != len(set(customer_groups)): frappe.throw( - _("Duplicate customer group found in the cutomer group table"), + _("Duplicate customer group found in the customer group table"), title=_("Duplicate Customer Group"), ) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index e8e8044929..61c01a4f97 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -339,7 +339,7 @@ { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Up To" }, { "fieldname": "col_break1", @@ -608,7 +608,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2023-02-14 04:53:34.887358", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index ca704908e8..300692f714 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -193,7 +193,7 @@ class PricingRule(Document): def validate_applicable_for_selling_or_buying(self): if not self.selling and not self.buying: - throw(_("Atleast one of the Selling or Buying must be selected")) + throw(_("At least one of the Selling or Buying must be selected")) if not self.selling and self.applicable_for in [ "Customer", diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json index 1d68b23d6c..7fdfdcd68a 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.json @@ -232,7 +232,7 @@ { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Up To" }, { "fieldname": "column_break_26", @@ -278,7 +278,7 @@ } ], "links": [], - "modified": "2021-05-06 16:20:22.039078", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme", diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 97fd4d040f..afa8bcbc83 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -51,7 +51,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", + "options": "\nTrialing\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", "read_only": 1 }, { @@ -267,7 +267,7 @@ "link_fieldname": "subscription" } ], - "modified": "2023-12-28 17:20:42.687789", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 6d27806253..9f19366667 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -78,9 +78,7 @@ class Subscription(Document): purchase_tax_template: DF.Link | None sales_tax_template: DF.Link | None start_date: DF.Date | None - status: DF.Literal[ - "", "Trialling", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed" - ] + status: DF.Literal["", "Trialing", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"] submit_invoice: DF.Check trial_period_end: DF.Date | None trial_period_start: DF.Date | None @@ -233,7 +231,7 @@ class Subscription(Document): Sets the status of the `Subscription` """ if self.is_trialling(): - self.status = "Trialling" + self.status = "Trialing" elif ( self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date) ): diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js index 6490ff3776..ea48b535cf 100644 --- a/erpnext/accounts/doctype/subscription/subscription_list.js +++ b/erpnext/accounts/doctype/subscription/subscription_list.js @@ -1,7 +1,7 @@ frappe.listview_settings['Subscription'] = { get_indicator: function(doc) { - if(doc.status === 'Trialling') { - return [__("Trialling"), "green"]; + if(doc.status === 'Trialing') { + return [__("Trialing"), "green"]; } else if(doc.status === 'Active') { return [__("Active"), "green"]; } else if(doc.status === 'Completed') { diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index a46642ad50..89be543057 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -46,7 +46,7 @@ class TestSubscription(FrappeTestCase): get_date_str(subscription.current_invoice_end), ) self.assertEqual(subscription.invoices, []) - self.assertEqual(subscription.status, "Trialling") + self.assertEqual(subscription.status, "Trialing") def test_create_subscription_without_trial_with_correct_period(self): subscription = create_subscription() diff --git a/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json b/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json index e2bf50d20a..b41012c883 100644 --- a/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/form_tour/accounts_settings/accounts_settings.json @@ -4,7 +4,7 @@ "doctype": "Form Tour", "idx": 0, "is_standard": 1, - "modified": "2021-06-29 17:00:26.145996", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -82,7 +82,7 @@ "label": "Accounts Frozen Till Date", "parent_field": "", "position": "Right", - "title": "Accounts Frozen Upto" + "title": "Accounts Frozen Up To" }, { "description": "Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.", diff --git a/erpnext/accounts/report/account_balance/account_balance.js b/erpnext/accounts/report/account_balance/account_balance.js index 5681be9211..ab5dce8145 100644 --- a/erpnext/accounts/report/account_balance/account_balance.js +++ b/erpnext/accounts/report/account_balance/account_balance.js @@ -39,7 +39,7 @@ frappe.query_reports["Account Balance"] = { { "value": "Asset Received But Not Billed", "label": __("Asset Received But Not Billed") }, { "value": "Bank", "label": __("Bank") }, { "value": "Cash", "label": __("Cash") }, - { "value": "Chargeble", "label": __("Chargeble") }, + { "value": "Chargeable", "label": __("Chargeable") }, { "value": "Capital Work in Progress", "label": __("Capital Work in Progress") }, { "value": "Cost of Goods Sold", "label": __("Cost of Goods Sold") }, { "value": "Depreciation", "label": __("Depreciation") }, diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 73572499f2..cc23d9dcb6 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1011,7 +1011,7 @@ def make_asset_movement(assets, purpose=None): assets = json.loads(assets) if len(assets) == 0: - frappe.throw(_("Atleast one asset has to be selected.")) + frappe.throw(_("At least one asset has to be selected.")) asset_movement = frappe.new_doc("Asset Movement") asset_movement.quantity = len(assets) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 063fe994aa..780f61f15c 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -40,7 +40,7 @@ class AssetMaintenance(Document): if getdate(task.next_due_date) < getdate(nowdate()): task.maintenance_status = "Overdue" if not task.assign_to and self.docstatus == 0: - throw(_("Row #{}: Please asign task to a member.").format(task.idx)) + throw(_("Row #{}: Please assign task to a member.").format(task.idx)) def on_update(self): for task in self.get("asset_maintenance_tasks"): diff --git a/erpnext/assets/onboarding_step/asset_category/asset_category.json b/erpnext/assets/onboarding_step/asset_category/asset_category.json index 58f322eecf..a1b68ba6b0 100644 --- a/erpnext/assets/onboarding_step/asset_category/asset_category.json +++ b/erpnext/assets/onboarding_step/asset_category/asset_category.json @@ -2,14 +2,14 @@ "action": "Show Form Tour", "action_label": "Let's review existing Asset Category", "creation": "2021-08-13 14:26:18.656303", - "description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipments\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n", + "description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipment\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-11-23 10:02:03.242127", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "name": "Asset Category", "owner": "Administrator", diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 45811a9344..e689b05246 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -202,7 +202,7 @@ def prepare_chart_data(data, filters): "values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()], }, { - "name": _("Depreciatied Amount"), + "name": _("Depreciated Amount"), "values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()], }, ], diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 6e50279d04..800e75630d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -141,7 +141,7 @@ def validate_returned_items(doc): items_returned = True if not items_returned: - frappe.throw(_("Atleast one item should be entered with negative quantity in return document")) + frappe.throw(_("At least one item should be entered with negative quantity in return document")) def validate_quantity(doc, args, ref, valid_items, already_returned_items): diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index ba1fae925e..8cba24afc9 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -155,7 +155,7 @@ class TallyMigration(Document): except RecursionError: self.log( _( - "Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name" + "Error occurred while parsing Chart of Accounts: Please make sure that no two accounts have the same name" ) ) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index ceb4406170..75d890c08d 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -222,7 +222,7 @@ class MaintenanceSchedule(TransactionBase): def validate_maintenance_detail(self): if not self.get("items"): - throw(_("Please enter Maintaince Details first")) + throw(_("Please enter Maintenance Details first")) for d in self.get("items"): if not d.item_code: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0acc2b1f91..aa7bc5bf76 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1520,7 +1520,7 @@ def validate_operation_data(row): if row.get("qty") > row.get("pending_qty"): frappe.throw( - _("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format( + _("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format( frappe.bold(row.get("operation")), frappe.bold(row.get("qty")), frappe.bold(row.get("pending_qty")), diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py index 72c8c074d2..95b5bc5ccd 100644 --- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -188,4 +188,4 @@ def execute(): raise err else: break - print(f"{processed} records have been sucessfully migrated") + print(f"{processed} records have been successfully migrated") diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json index d332b4e76b..ecc198ab9e 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -64,7 +64,7 @@ { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto", + "label": "Valid Up To", "reqd": 1 }, { @@ -135,7 +135,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-04-18 08:25:35.302081", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Regional", "name": "Lower Deduction Certificate", diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index 72b3a49d2a..6982ad129f 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -37,7 +37,7 @@ class LowerDeductionCertificate(Document): def validate_dates(self): if getdate(self.valid_upto) < getdate(self.valid_from): - frappe.throw(_("Valid Upto date cannot be before Valid From date")) + frappe.throw(_("Valid Up To date cannot be before Valid From date")) fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) @@ -45,7 +45,7 @@ class LowerDeductionCertificate(Document): frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) if not (fiscal_year.year_start_date <= getdate(self.valid_upto) <= fiscal_year.year_end_date): - frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + frappe.throw(_("Valid Up To date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) def validate_supplier_against_tax_category(self): duplicate_certificate = frappe.db.get_value( diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 29dbd4f321..47153a8e0c 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -427,11 +427,11 @@ def create_internal_customer( if not allowed_to_interact_with: allowed_to_interact_with = represents_company - exisiting_representative = frappe.db.get_value( + existing_representative = frappe.db.get_value( "Customer", {"represents_company": represents_company} ) - if exisiting_representative: - return exisiting_representative + if existing_representative: + return existing_representative if not frappe.db.exists("Customer", customer_name): customer = frappe.get_doc( diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 5d1d7695eb..79f24d1160 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -643,7 +643,7 @@ class SalesOrder(SellingController): if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): frappe.throw( _( - "Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No" + "Item {0} has no Serial No. Only serialized items can have delivery based on Serial No" ).format(item.item_code) ) if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index feecd9cfd8..80e1c20ad9 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -360,7 +360,7 @@ erpnext.PointOfSale.Controller = class { this.order_summary.load_summary_of(this.frm.doc, true); frappe.show_alert({ indicator: 'green', - message: __('POS invoice {0} created succesfully', [r.doc.name]) + message: __('POS invoice {0} created successfully', [r.doc.name]) }); }); } diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 9446fb46a4..27313ae676 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -54,7 +54,7 @@ class AuthorizationControl(TransactionBase): if not has_common(appr_roles, frappe.get_roles()) and not has_common( appr_users, [session["user"]] ): - frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on))) + frappe.msgprint(_("Not authorized since {0} exceeds limits").format(_(based_on))) frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users))) def validate_auth_rule(self, doctype_name, total, based_on, cond, company, master_name=""): diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index daf2df5a59..fc1fc9b7ae 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -441,13 +441,13 @@ { "fieldname": "prefered_contact_email", "fieldtype": "Select", - "label": "Prefered Contact Email", + "label": "Preferred Contact Email", "options": "\nCompany Email\nPersonal Email\nUser ID" }, { "fieldname": "prefered_email", "fieldtype": "Data", - "label": "Prefered Email", + "label": "Preferred Email", "options": "Email", "read_only": 1 }, @@ -524,7 +524,7 @@ { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Up To" }, { "fieldname": "place_of_issue", @@ -824,7 +824,7 @@ "image_field": "image", "is_tree": 1, "links": [], - "modified": "2024-01-03 17:36:20.984421", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7d7b0cd476..58990d4838 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -796,36 +796,36 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): updated_dn = [] for dnd in dn_details: - billed_amt_agianst_dn = 0 + billed_amt_against_dn = 0 # If delivered against Sales Invoice if dnd.si_detail: - billed_amt_agianst_dn = flt(dnd.amount) - billed_against_so -= billed_amt_agianst_dn + billed_amt_against_dn = flt(dnd.amount) + billed_against_so -= billed_amt_against_dn else: # Get billed amount directly against Delivery Note - billed_amt_agianst_dn = frappe.db.sql( + billed_amt_against_dn = frappe.db.sql( """select sum(amount) from `tabSales Invoice Item` where dn_detail=%s and docstatus=1""", dnd.name, ) - billed_amt_agianst_dn = billed_amt_agianst_dn and billed_amt_agianst_dn[0][0] or 0 + billed_amt_against_dn = billed_amt_against_dn and billed_amt_against_dn[0][0] or 0 # Distribute billed amount directly against SO between DNs based on FIFO - if billed_against_so and billed_amt_agianst_dn < dnd.amount: - pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn + if billed_against_so and billed_amt_against_dn < dnd.amount: + pending_to_bill = flt(dnd.amount) - billed_amt_against_dn if pending_to_bill <= billed_against_so: - billed_amt_agianst_dn += pending_to_bill + billed_amt_against_dn += pending_to_bill billed_against_so -= pending_to_bill else: - billed_amt_agianst_dn += billed_against_so + billed_amt_against_dn += billed_against_so billed_against_so = 0 frappe.db.set_value( "Delivery Note Item", dnd.name, "billed_amt", - billed_amt_agianst_dn, + billed_amt_against_dn, update_modified=update_modified, ) diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index f4d9bb0742..2390ee2438 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -191,7 +191,7 @@ { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto" + "label": "Valid Up To" }, { "fieldname": "section_break_24", @@ -220,7 +220,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-11-15 08:26:04.041861", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 89a130a6bf..de2add64ef 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -59,7 +59,7 @@ class ItemPrice(Document): def validate_dates(self): if self.valid_from and self.valid_upto: if getdate(self.valid_from) > getdate(self.valid_upto): - frappe.throw(_("Valid From Date must be lesser than Valid Upto Date.")) + frappe.throw(_("Valid From Date must be lesser than Valid Up To Date.")) def update_price_list_details(self): if self.price_list: diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index 8fd4938fa3..63d717c312 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -64,7 +64,7 @@ class TestItemPrice(FrappeTestCase): # Enter invalid dates valid_from >= valid_upto doc.valid_from = "2017-04-20" doc.valid_upto = "2017-04-17" - # Valid Upto Date can not be less/equal than Valid From Date + # Valid Up To Date can not be less/equal than Valid From Date self.assertRaises(frappe.ValidationError, doc.save) def test_price_in_a_qty(self): diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index ad9b34c9ac..e784b700ed 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -776,7 +776,7 @@ def raise_work_orders(material_request): ) else: msgprint( - _("The {0} {1} created sucessfully").format(frappe.bold(_("Work Order")), work_orders_list[0]) + _("The {0} {1} created successfully").format(frappe.bold(_("Work Order")), work_orders_list[0]) ) if errors: diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index de7a3d05bf..c85bd715f2 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -24,7 +24,7 @@ frappe.listview_settings['Material Request'] = { } else if (doc.material_request_type == "Purchase") { return [__("Ordered"), "green", "per_ordered,=,100"]; } else if (doc.material_request_type == "Material Transfer") { - return [__("Transfered"), "green", "per_ordered,=,100"]; + return [__("Transferred"), "green", "per_ordered,=,100"]; } else if (doc.material_request_type == "Material Issue") { return [__("Issued"), "green", "per_ordered,=,100"]; } else if (doc.material_request_type == "Customer Provided") { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index fcb7a6d510..bf6080bf23 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -951,32 +951,32 @@ def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=No billed_against_po = flt(po_billed_amt_details.get(pr_item.purchase_order_item)) # Get billed amount directly against Purchase Receipt - billed_amt_agianst_pr = flt(pr_items_billed_amount.get(pr_item.name, 0)) + billed_amt_against_pr = flt(pr_items_billed_amount.get(pr_item.name, 0)) # Distribute billed amount directly against PO between PRs based on FIFO - if billed_against_po and billed_amt_agianst_pr < pr_item.amount: - pending_to_bill = flt(pr_item.amount) - billed_amt_agianst_pr + if billed_against_po and billed_amt_against_pr < pr_item.amount: + pending_to_bill = flt(pr_item.amount) - billed_amt_against_pr if pending_to_bill <= billed_against_po: - billed_amt_agianst_pr += pending_to_bill + billed_amt_against_pr += pending_to_bill billed_against_po -= pending_to_bill else: - billed_amt_agianst_pr += billed_against_po + billed_amt_against_pr += billed_against_po billed_against_po = 0 po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po - if pr_item.billed_amt != billed_amt_agianst_pr: + if pr_item.billed_amt != billed_amt_against_pr: # update existing doc if possible if pr_doc and pr_item.parent == pr_doc.name: pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None) - pr_item.db_set("billed_amt", billed_amt_agianst_pr, update_modified=update_modified) + pr_item.db_set("billed_amt", billed_amt_against_pr, update_modified=update_modified) else: frappe.db.set_value( "Purchase Receipt Item", pr_item.name, "billed_amt", - billed_amt_agianst_pr, + billed_amt_against_pr, update_modified=update_modified, ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 96c249fee4..c371b7036b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -640,7 +640,7 @@ class StockEntry(StockController): frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx)) if not (d.s_warehouse or d.t_warehouse): - frappe.throw(_("Atleast one warehouse is mandatory")) + frappe.throw(_("At least one warehouse is mandatory")) def validate_work_order(self): if self.purpose in ( diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 122829032d..f84456abae 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -176,7 +176,7 @@ "description": "No stock transactions can be created or modified before this date.", "fieldname": "stock_frozen_upto", "fieldtype": "Date", - "label": "Stock Frozen Upto" + "label": "Stock Frozen Up To" }, { "description": "Stock transactions that are older than the mentioned days cannot be modified.", @@ -427,7 +427,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-18 12:35:30.068799", + "modified": "2024-01-24 02:20:26.145996", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 4cd9cbb9d5..1f5f41a4d3 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -246,7 +246,7 @@ def notify_errors(exceptions_list): _("Dear System Manager,") + "
" + _( - "An error occured for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :" + "An error occurred for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :" ) + "
" ) diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index 4c8379e41c..581b53ddd9 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -124,7 +124,7 @@ def get_help_messages(): doctype="Timesheet", title=_("Add Timesheets"), description=_( - "Timesheets help keep track of time, cost and billing for activites done by your team" + "Timesheets help keep track of time, cost and billing for activities done by your team" ), action=_("Create Timesheet"), route="List/Timesheet", From b046d980ad83b906d46515082bd32038daadcb3c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Jan 2024 10:29:55 +0530 Subject: [PATCH 53/57] fix: not able to edit address through portal --- erpnext/utilities/web_form/addresses/addresses.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/utilities/web_form/addresses/addresses.json b/erpnext/utilities/web_form/addresses/addresses.json index 2f5e180731..4e2d8e36c2 100644 --- a/erpnext/utilities/web_form/addresses/addresses.json +++ b/erpnext/utilities/web_form/addresses/addresses.json @@ -8,26 +8,29 @@ "allow_print": 0, "amount": 0.0, "amount_based_on_field": 0, + "anonymous": 0, + "apply_document_permissions": 1, + "condition_json": "[]", "creation": "2016-06-24 15:50:33.196990", "doc_type": "Address", "docstatus": 0, "doctype": "Web Form", "idx": 0, "is_standard": 1, + "list_columns": [], + "list_title": "", "login_required": 1, "max_attachment_size": 0, - "modified": "2019-10-15 06:55:30.405119", - "modified_by": "Administrator", + "modified": "2024-01-24 10:28:35.026064", + "modified_by": "rohitw1991@gmail.com", "module": "Utilities", "name": "addresses", "owner": "Administrator", "published": 1, "route": "address", - "route_to_success_link": 0, "show_attachments": 0, - "show_in_grid": 0, + "show_list": 1, "show_sidebar": 0, - "sidebar_items": [], "success_url": "/addresses", "title": "Address", "web_form_fields": [ From 764f3422a0826e22d116616daa3849d02cc1e117 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Jan 2024 11:45:15 +0530 Subject: [PATCH 54/57] fix: email list for auto reorder material request --- .../material_request/test_material_request.py | 56 +++++++++++++++++ erpnext/stock/reorder_item.py | 61 +++++++++++++++---- 2 files changed, 106 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 3e440497f0..48397a384d 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -774,6 +774,62 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.per_ordered, 100) self.assertEqual(existing_requested_qty, current_requested_qty) + def test_auto_email_users_with_company_user_permissions(self): + from erpnext.stock.reorder_item import get_email_list + + comapnywise_users = { + "_Test Company": "test_auto_email_@example.com", + "_Test Company 1": "test_auto_email_1@example.com", + } + + permissions = [] + + for company, user in comapnywise_users.items(): + if not frappe.db.exists("User", user): + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": user, + "send_notifications": 0, + "enabled": 1, + "user_type": "System User", + "roles": [{"role": "Purchase Manager"}], + } + ).insert(ignore_permissions=True) + + if not frappe.db.exists( + "User Permission", {"user": user, "allow": "Company", "for_value": company} + ): + perm_doc = frappe.get_doc( + { + "doctype": "User Permission", + "user": user, + "allow": "Company", + "for_value": company, + "apply_to_all_doctypes": 1, + } + ).insert(ignore_permissions=True) + + permissions.append(perm_doc) + + comapnywise_mr_list = frappe._dict({}) + mr1 = make_material_request() + comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name) + + mr2 = make_material_request( + company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1" + ) + comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name) + + for company, mr_list in comapnywise_mr_list.items(): + emails = get_email_list(company) + + self.assertTrue(comapnywise_users[company] in emails) + + for perm in permissions: + perm.delete() + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 4cd9cbb9d5..31fb99ab20 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -145,6 +145,7 @@ def create_material_request(material_requests): mr.log_error("Unable to create material request") + company_wise_mr = frappe._dict({}) for request_type in material_requests: for company in material_requests[request_type]: try: @@ -206,17 +207,19 @@ def create_material_request(material_requests): mr.submit() mr_list.append(mr) + company_wise_mr.setdefault(company, []).append(mr) + except Exception: _log_exception(mr) - if mr_list: + if company_wise_mr: if getattr(frappe.local, "reorder_email_notify", None) is None: frappe.local.reorder_email_notify = cint( frappe.db.get_single_value("Stock Settings", "reorder_email_notify") ) if frappe.local.reorder_email_notify: - send_email_notification(mr_list) + send_email_notification(company_wise_mr) if exceptions_list: notify_errors(exceptions_list) @@ -224,20 +227,56 @@ def create_material_request(material_requests): return mr_list -def send_email_notification(mr_list): +def send_email_notification(company_wise_mr): """Notify user about auto creation of indent""" - email_list = frappe.db.sql_list( - """select distinct r.parent - from `tabHas Role` r, tabUser p - where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 - and r.role in ('Purchase Manager','Stock Manager') - and p.name not in ('Administrator', 'All', 'Guest')""" + for company, mr_list in company_wise_mr.items(): + email_list = get_email_list(company) + + if not email_list: + continue + + msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + + frappe.sendmail( + recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg + ) + + +def get_email_list(company): + users = get_comapny_wise_users(company) + user_table = frappe.qb.DocType("User") + role_table = frappe.qb.DocType("Has Role") + + query = ( + frappe.qb.from_(user_table) + .inner_join(role_table) + .on(user_table.name == role_table.parent) + .select(user_table.email) + .where( + (role_table.role.isin(["Purchase Manager", "Stock Manager"])) + & (user_table.name.notin(["Administrator", "All", "Guest"])) + & (user_table.enabled == 1) + & (user_table.docstatus < 2) + ) ) - msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + if users: + query = query.where(user_table.name.isin(users)) - frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) + emails = query.run(as_dict=True) + + return list(set([email.email for email in emails])) + + +def get_comapny_wise_users(company): + users = frappe.get_all( + "User Permission", + filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1}, + fields=["user"], + ) + + return [user.user for user in users] def notify_errors(exceptions_list): From b127aa308eeda0246cdf885c36f0b36f270e2ae1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 24 Jan 2024 11:42:37 +0530 Subject: [PATCH 55/57] fix: AttributeError in company transaction deletion --- erpnext/setup/doctype/company/company.py | 2 +- .../test_transaction_deletion_record.py | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 68a3854b0d..876b6a4ac8 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -908,8 +908,8 @@ def generate_id_for_deletion_job(company): @frappe.whitelist() def is_deletion_job_running(company): job_id = generate_id_for_deletion_job(company) - job_name = get_job(job_id).get_id() # job name will have site prefix if is_job_enqueued(job_id): + job_name = get_job(job_id).get_id() # job name will have site prefix frappe.throw( _("A Transaction Deletion Job: {0} is already running for {1}").format( frappe.bold(get_link_to_form("RQ Job", job_name)), frappe.bold(company) diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index 319d435ca6..844e7865e3 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -4,9 +4,10 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase -class TestTransactionDeletionRecord(unittest.TestCase): +class TestTransactionDeletionRecord(FrappeTestCase): def setUp(self): create_company("Dunder Mifflin Paper Co") @@ -14,7 +15,7 @@ class TestTransactionDeletionRecord(unittest.TestCase): frappe.db.rollback() def test_doctypes_contain_company_field(self): - tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") + tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: contains_company = False doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"] @@ -27,17 +28,27 @@ class TestTransactionDeletionRecord(unittest.TestCase): def test_no_of_docs_is_correct(self): for i in range(5): create_task("Dunder Mifflin Paper Co") - tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") + tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: if doctype.doctype_name == "Task": self.assertEqual(doctype.no_of_docs, 5) def test_deletion_is_successful(self): create_task("Dunder Mifflin Paper Co") - create_transaction_deletion_request("Dunder Mifflin Paper Co") + create_transaction_deletion_doc("Dunder Mifflin Paper Co") tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"}) self.assertEqual(tasks_containing_company, []) + def test_company_transaction_deletion_request(self): + from erpnext.setup.doctype.company.company import create_transaction_deletion_request + + # don't reuse below company for other test cases + company = "Deep Space Exploration" + create_company(company) + + # below call should not raise any exceptions or throw errors + create_transaction_deletion_request(company) + def create_company(company_name): company = frappe.get_doc( @@ -46,7 +57,7 @@ def create_company(company_name): company.insert(ignore_if_duplicate=True) -def create_transaction_deletion_request(company): +def create_transaction_deletion_doc(company): tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() From 64cb1153de2d09883234e4e80e5306de27db328f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 15 Jan 2024 19:39:41 +0530 Subject: [PATCH 56/57] fix: incorrect active serial nos --- .../js/utils/serial_no_batch_selector.js | 4 + .../serial_and_batch_bundle.py | 16 ++- .../stock_reconciliation.py | 88 ++++++++++-- .../test_stock_reconciliation.py | 68 +++++++++ erpnext/stock/stock_ledger.py | 129 ++++++++++++++---- 5 files changed, 263 insertions(+), 42 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 44a4957b41..80ade7086c 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { let warehouse = this.item?.type_of_transaction === "Outward" ? (this.item.warehouse || this.item.s_warehouse) : ""; + if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') { + warehouse = this.get_warehouse(); + } + return { 'item_code': this.item.item_code, 'warehouse': ["=", warehouse] diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 63cc938c09..9cad8f62b8 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -250,6 +250,7 @@ class SerialandBatchBundle(Document): for d in self.entries: available_qty = 0 + if self.has_serial_no: d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)) else: @@ -892,6 +893,13 @@ class SerialandBatchBundle(Document): elif batch_nos: self.set("entries", batch_nos) + def delete_serial_batch_entries(self): + SBBE = frappe.qb.DocType("Serial and Batch Entry") + + frappe.qb.from_(SBBE).delete().where(SBBE.parent == self.name).run() + + self.set("entries", []) + @frappe.whitelist() def download_blank_csv_template(content): @@ -1374,10 +1382,12 @@ def get_available_serial_nos(kwargs): elif kwargs.based_on == "Expiry": order_by = "amc_expiry_date asc" - filters = {"item_code": kwargs.item_code, "warehouse": ("is", "set")} + filters = {"item_code": kwargs.item_code} - if kwargs.warehouse: - filters["warehouse"] = kwargs.warehouse + if not kwargs.get("ignore_warehouse"): + filters["warehouse"] = ("is", "set") + if kwargs.warehouse: + filters["warehouse"] = kwargs.warehouse # Since SLEs are not present against Reserved Stock [POS invoices, SRE], need to ignore reserved serial nos. ignore_serial_nos = get_reserved_serial_nos(kwargs) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 6819968394..788ae0d3ab 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -156,6 +156,7 @@ class StockReconciliation(StockController): "warehouse": item.warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, + "ignore_warehouse": 1, } ) ) @@ -780,7 +781,20 @@ class StockReconciliation(StockController): current_qty = 0.0 if row.current_serial_and_batch_bundle: - current_qty = self.get_qty_for_serial_and_batch_bundle(row) + current_qty = self.get_current_qty_for_serial_or_batch(row) + elif row.serial_no: + item_dict = get_stock_balance_for( + row.item_code, + row.warehouse, + self.posting_date, + self.posting_time, + voucher_no=self.name, + ) + + current_qty = item_dict.get("qty") + row.current_serial_no = item_dict.get("serial_nos") + row.current_valuation_rate = item_dict.get("rate") + val_rate = item_dict.get("rate") elif row.batch_no: current_qty = get_batch_qty_for_stock_reco( row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name @@ -788,15 +802,16 @@ class StockReconciliation(StockController): precesion = row.precision("current_qty") if flt(current_qty, precesion) != flt(row.current_qty, precesion): - val_rate = get_valuation_rate( - row.item_code, - row.warehouse, - self.doctype, - self.name, - company=self.company, - batch_no=row.batch_no, - serial_and_batch_bundle=row.current_serial_and_batch_bundle, - ) + if not row.serial_no: + val_rate = get_valuation_rate( + row.item_code, + row.warehouse, + self.doctype, + self.name, + company=self.company, + batch_no=row.batch_no, + serial_and_batch_bundle=row.current_serial_and_batch_bundle, + ) row.current_valuation_rate = val_rate row.current_qty = current_qty @@ -842,11 +857,56 @@ class StockReconciliation(StockController): return allow_negative_stock - def get_qty_for_serial_and_batch_bundle(self, row): + def get_current_qty_for_serial_or_batch(self, row): doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle) - precision = doc.entries[0].precision("qty") + current_qty = 0.0 + if doc.has_serial_no: + current_qty = self.get_current_qty_for_serial_nos(doc) + elif doc.has_batch_no: + current_qty = self.get_current_qty_for_batch_nos(doc) - current_qty = 0 + return abs(current_qty) + + def get_current_qty_for_serial_nos(self, doc): + serial_nos_details = get_available_serial_nos( + frappe._dict( + { + "item_code": doc.item_code, + "warehouse": doc.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_no": self.name, + "ignore_warehouse": 1, + } + ) + ) + + if not serial_nos_details: + return 0.0 + + doc.delete_serial_batch_entries() + current_qty = 0.0 + for serial_no_row in serial_nos_details: + current_qty += 1 + doc.append( + "entries", + { + "serial_no": serial_no_row.serial_no, + "qty": -1, + "warehouse": doc.warehouse, + "batch_no": serial_no_row.batch_no, + }, + ) + + doc.set_incoming_rate(save=True) + doc.calculate_qty_and_amount(save=True) + doc.db_update_all() + + return current_qty + + def get_current_qty_for_batch_nos(self, doc): + current_qty = 0.0 + precision = doc.entries[0].precision("qty") for d in doc.entries: qty = ( get_batch_qty( @@ -864,7 +924,7 @@ class StockReconciliation(StockController): current_qty += qty - return abs(current_qty) + return current_qty def get_batch_qty_for_stock_reco( diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 70e9fb2205..0bbfed40d8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -925,6 +925,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(len(serial_batch_bundle), 0) + def test_backdated_purchase_receipt_with_stock_reco(self): + item_code = self.make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TEST-SERIAL-.###", + } + ).name + + warehouse = "_Test Warehouse - _TC" + + # Step - 1: Create a Backdated Purchase Receipt + + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) + ) + pr1.reload() + + serial_nos = sorted(get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle))[:5] + + # Step - 2: Create a Stock Reconciliation + sr1 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=5, + serial_no=serial_nos, + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -10.0) + self.assertAlmostEqual(d.stock_value_difference, -1000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + # Step - 3: Create a Purchase Receipt before the first Purchase Receipt + make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5) + ) + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["serial_no", "actual_qty", "stock_value_difference"], + filters={"voucher_no": sr1.name, "is_cancelled": 0}, + order_by="creation", + ) + + for d in data: + if d.actual_qty < 0: + self.assertEqual(d.actual_qty, -20.0) + self.assertAlmostEqual(d.stock_value_difference, -3000.0) + else: + self.assertEqual(d.actual_qty, 5.0) + self.assertAlmostEqual(d.stock_value_difference, 500.0) + + active_serial_no = frappe.get_all( + "Serial No", filters={"status": "Active", "item_code": item_code} + ) + self.assertEqual(len(active_serial_no), 5) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0370666263..45764f3ec0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -9,9 +9,18 @@ from typing import Optional, Set, Tuple import frappe from frappe import _, scrub from frappe.model.meta import get_field_precision -from frappe.query_builder import Case from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json +from frappe.utils import ( + cint, + cstr, + flt, + get_link_to_form, + getdate, + now, + nowdate, + nowtime, + parse_json, +) import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -712,11 +721,10 @@ class update_entries_after(object): if ( sle.voucher_type == "Stock Reconciliation" - and ( - sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no) - ) + and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle) and sle.voucher_detail_no and not self.args.get("sle_id") + and sle.is_cancelled == 0 ): self.reset_actual_qty_for_stock_reco(sle) @@ -737,6 +745,23 @@ class update_entries_after(object): if sle.serial_and_batch_bundle: self.calculate_valuation_for_serial_batch_bundle(sle) + elif sle.serial_no and not self.args.get("sle_id"): + # Only run in reposting + self.get_serialized_values(sle) + self.wh_data.qty_after_transaction += flt(sle.actual_qty) + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + self.wh_data.qty_after_transaction = sle.qty_after_transaction + + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) + elif ( + sle.batch_no + and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True) + and not self.args.get("sle_id") + ): + # Only run in reposting + self.update_batched_values(sle) else: if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: # assert @@ -782,6 +807,45 @@ class update_entries_after(object): ): self.update_outgoing_rate_on_transaction(sle) + def get_serialized_values(self, sle): + incoming_rate = flt(sle.incoming_rate) + actual_qty = flt(sle.actual_qty) + serial_nos = cstr(sle.serial_no).split("\n") + + if incoming_rate < 0: + # wrong incoming rate + incoming_rate = self.wh_data.valuation_rate + + stock_value_change = 0 + if actual_qty > 0: + stock_value_change = actual_qty * incoming_rate + else: + # In case of delivery/stock issue, get average purchase rate + # of serial nos of current entry + if not sle.is_cancelled: + outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos) + stock_value_change = -1 * outgoing_value + else: + stock_value_change = actual_qty * sle.outgoing_rate + + new_stock_qty = self.wh_data.qty_after_transaction + actual_qty + + if new_stock_qty > 0: + new_stock_value = ( + self.wh_data.qty_after_transaction * self.wh_data.valuation_rate + ) + stock_value_change + if new_stock_value >= 0: + # calculate new valuation rate only if stock value is positive + # else it remains the same as that of previous entry + self.wh_data.valuation_rate = new_stock_value / new_stock_qty + + if not self.wh_data.valuation_rate and sle.voucher_detail_no: + allow_zero_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) + if not allow_zero_rate: + self.wh_data.valuation_rate = self.get_fallback_rate(sle) + def reset_actual_qty_for_stock_reco(self, sle): doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) @@ -795,6 +859,36 @@ class update_entries_after(object): if abs(sle.actual_qty) == 0.0: sle.is_cancelled = 1 + if sle.serial_and_batch_bundle and frappe.get_cached_value( + "Item", sle.item_code, "has_serial_no" + ): + self.update_serial_no_status(sle) + + def update_serial_no_status(self, sle): + from erpnext.stock.serial_batch_bundle import get_serial_nos + + serial_nos = get_serial_nos(sle.serial_and_batch_bundle) + if not serial_nos: + return + + warehouse = None + status = "Inactive" + + if sle.actual_qty > 0: + warehouse = sle.warehouse + status = "Active" + + sn_table = frappe.qb.DocType("Serial No") + + query = ( + frappe.qb.update(sn_table) + .set(sn_table.warehouse, warehouse) + .set(sn_table.status, status) + .where(sn_table.name.isin(serial_nos)) + ) + + query.run() + def calculate_valuation_for_serial_batch_bundle(self, sle): doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) @@ -1171,11 +1265,12 @@ class update_entries_after(object): outgoing_rate = get_batch_incoming_rate( item_code=sle.item_code, warehouse=sle.warehouse, - serial_and_batch_bundle=sle.serial_and_batch_bundle, + batch_no=sle.batch_no, posting_date=sle.posting_date, posting_time=sle.posting_time, creation=sle.creation, ) + if outgoing_rate is None: # This can *only* happen if qty available for the batch is zero. # in such case fall back various other rates. @@ -1449,11 +1544,10 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): def get_batch_incoming_rate( - item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None + item_code, warehouse, batch_no, posting_date, posting_time, creation=None ): sle = frappe.qb.DocType("Stock Ledger Entry") - batch_ledger = frappe.qb.DocType("Serial and Batch Entry") timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( posting_date, posting_time @@ -1464,28 +1558,13 @@ def get_batch_incoming_rate( == CombineDatetime(posting_date, posting_time) ) & (sle.creation < creation) - batches = frappe.get_all( - "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle} - ) - batch_details = ( frappe.qb.from_(sle) - .inner_join(batch_ledger) - .on(sle.serial_and_batch_bundle == batch_ledger.parent) - .select( - Sum( - Case() - .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate) - .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1) - ).as_("batch_value"), - Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_( - "batch_qty" - ), - ) + .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty")) .where( (sle.item_code == item_code) & (sle.warehouse == warehouse) - & (batch_ledger.batch_no.isin([row.batch_no for row in batches])) + & (sle.batch_no == batch_no) & (sle.is_cancelled == 0) ) .where(timestamp_condition) From 7d3240ae3a2e8fc761d460793fbddfb2b5cffc09 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Jan 2024 23:45:06 +0530 Subject: [PATCH 57/57] fix: Item Tax template is not working for e-commerce --- erpnext/controllers/accounts_controller.py | 32 +++++++++++ .../doctype/quotation/test_quotation.py | 55 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index afbea61052..7dfcb30ff5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -22,6 +22,7 @@ from frappe.utils import ( get_link_to_form, getdate, nowdate, + parse_json, today, ) @@ -833,6 +834,37 @@ class AccountsController(TransactionBase): self.extend("taxes", get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges"))) + def append_taxes_from_item_tax_template(self): + if not frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"): + return + + for row in self.items: + item_tax_rate = row.get("item_tax_rate") + if not item_tax_rate: + continue + + if isinstance(item_tax_rate, str): + item_tax_rate = parse_json(item_tax_rate) + + for account_head, rate in item_tax_rate.items(): + row = self.get_tax_row(account_head) + + if not row: + self.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": account_head, + "rate": 0, + "description": account_head, + }, + ) + + def get_tax_row(self, account_head): + for row in self.taxes: + if row.account_head == account_head: + return row + def set_other_charges(self): self.set("taxes", []) self.set_taxes() diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 2a4855e318..86c7a72f73 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -609,6 +609,61 @@ class TestQuotation(FrappeTestCase): quotation.items[0].conversion_factor = 2.23 self.assertRaises(frappe.ValidationError, quotation.save) + def test_item_tax_template_for_quotation(self): + from erpnext.stock.doctype.item.test_item import make_item + + if not frappe.db.exists("Account", {"account_name": "_Test Vat", "company": "_Test Company"}): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "_Test Vat", + "company": "_Test Company", + "account_type": "Tax", + "root_type": "Asset", + "is_group": 0, + "parent_account": "Tax Assets - _TC", + "tax_rate": 10, + } + ).insert() + + if not frappe.db.exists("Item Tax Template", "Vat Template - _TC"): + doc = frappe.get_doc( + { + "doctype": "Item Tax Template", + "name": "Vat Template", + "title": "Vat Template", + "company": "_Test Company", + "taxes": [ + { + "tax_type": "_Test Vat - _TC", + "tax_rate": 5, + } + ], + } + ).insert() + + item_doc = make_item("_Test Item Tax Template QTN", {"is_stock_item": 1}) + if not frappe.db.exists( + "Item Tax", {"parent": item_doc.name, "item_tax_template": "Vat Template - _TC"} + ): + item_doc.append("taxes", {"item_tax_template": "Vat Template - _TC"}) + item_doc.save() + + quotation = make_quotation( + item_code="_Test Item Tax Template QTN", qty=1, rate=100, do_not_submit=1 + ) + self.assertFalse(quotation.taxes) + + quotation.append_taxes_from_item_tax_template() + quotation.save() + self.assertTrue(quotation.taxes) + for row in quotation.taxes: + self.assertEqual(row.account_head, "_Test Vat - _TC") + self.assertAlmostEqual(row.base_tax_amount, quotation.total * 5 / 100) + + item_doc.taxes = [] + item_doc.save() + test_records = frappe.get_test_records("Quotation")