From c809e61103f84a42f23a1bb4a3d1fb56ebf5cff3 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Tue, 10 Oct 2023 20:40:50 +0200 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 c88ce552425f077f59e98458799819ff3dd72742 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 22 Jan 2024 22:04:30 +0100 Subject: [PATCH 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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