From e730b8c6e4e38d241f522108519767e4db4cc048 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 25 Mar 2024 16:11:10 +0100 Subject: [PATCH 01/39] fix(return): set default return warehouse This captures the case of manual modifications to the return and ensures that by default, the correct return warehouse will be set (cherry picked from commit fa65291e9877464aa650371c21c9a355f91483c3) --- erpnext/controllers/sales_and_purchase_return.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 3755506b6f5..8e80b15e972 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -335,6 +335,9 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note")) if source.tax_withholding_category: doc.set_onload("supplier_tds", source.tax_withholding_category) + elif doctype == "Delivery Note": + # manual additions to the return should hit the return warehous, too + doc.set_warehouse = default_warehouse_for_sales_return for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": From 4a749cec72c060f98662191f3dcaf801496d0ab9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:49:26 +0100 Subject: [PATCH 02/39] feat: remove Payroll Entry from Bank Account dashboard (backport #43931) (#43933) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/accounts/doctype/bank_account/bank_account.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index a5a7691eb76..962551b2417 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -224,11 +224,6 @@ "link_doctype": "Bank Guarantee", "link_fieldname": "bank_account" }, - { - "group": "Transactions", - "link_doctype": "Payroll Entry", - "link_fieldname": "bank_account" - }, { "group": "Transactions", "link_doctype": "Bank Transaction", @@ -255,7 +250,7 @@ "link_fieldname": "default_bank_account" } ], - "modified": "2024-09-24 06:57:41.292970", + "modified": "2024-10-30 09:41:14.113414", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", From b343334f694b79a92faabbba03984dca086ca8c0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 16 Oct 2024 17:01:47 +0530 Subject: [PATCH 03/39] feat: advance payment ledger doctype (cherry picked from commit 2d6efd7cc83e97186e07457567b1439b14b1fb57) --- .../advance_payment_ledger_entry/__init__.py | 0 .../advance_payment_ledger_entry.js | 8 ++ .../advance_payment_ledger_entry.json | 89 +++++++++++++++++++ .../advance_payment_ledger_entry.py | 27 ++++++ .../test_advance_payment_ledger_entry.py | 29 ++++++ 5 files changed, 153 insertions(+) create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/__init__.py create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/__init__.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js new file mode 100644 index 00000000000..1a0dc1e7272 --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Advance Payment Ledger Entry", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json new file mode 100644 index 00000000000..6f4792543d8 --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-16 16:57:12.085072", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "company", + "voucher_type", + "voucher_no", + "against_voucher_type", + "against_voucher_no", + "amount", + "currency", + "event" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "against_voucher_type", + "fieldtype": "Link", + "label": "Against Voucher Type", + "options": "DocType" + }, + { + "fieldname": "against_voucher_no", + "fieldtype": "Dynamic Link", + "label": "Against Voucher No", + "options": "against_voucher_type" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "event", + "fieldtype": "Data", + "label": "Event" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-10-16 17:08:09.334330", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Advance Payment Ledger Entry", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py new file mode 100644 index 00000000000..0ec2d411761 --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AdvancePaymentLedgerEntry(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + against_voucher_no: DF.DynamicLink | None + against_voucher_type: DF.Link | None + amount: DF.Currency + company: DF.Link | None + currency: DF.Link | None + event: DF.Data | None + voucher_no: DF.DynamicLink | None + voucher_type: DF.Link | None + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py new file mode 100644 index 00000000000..ef938062399 --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class TestAdvancePaymentLedgerEntry(UnitTestCase): + """ + Unit tests for AdvancePaymentLedgerEntry. + Use this class for testing individual functions and methods. + """ + + pass + + +class TestAdvancePaymentLedgerEntry(IntegrationTestCase): + """ + Integration tests for AdvancePaymentLedgerEntry. + Use this class for testing interactions between multiple components. + """ + + pass From 0d02f8b5d127b6f2dfcf8bf36a2bd6d1bb19aa0d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 16 Oct 2024 17:11:50 +0530 Subject: [PATCH 04/39] refactor: make all fields readonly (cherry picked from commit f176a82198b894bfc289fcf8e506ce0d2c918573) --- .../advance_payment_ledger_entry.json | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json index 6f4792543d8..1d0a5b42a31 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -19,52 +19,60 @@ "fieldname": "voucher_type", "fieldtype": "Link", "label": "Voucher Type", - "options": "DocType" + "options": "DocType", + "read_only": 1 }, { "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "label": "Voucher No", - "options": "voucher_type" + "options": "voucher_type", + "read_only": 1 }, { "fieldname": "against_voucher_type", "fieldtype": "Link", "label": "Against Voucher Type", - "options": "DocType" + "options": "DocType", + "read_only": 1 }, { "fieldname": "against_voucher_no", "fieldtype": "Dynamic Link", "label": "Against Voucher No", - "options": "against_voucher_type" + "options": "against_voucher_type", + "read_only": 1 }, { "fieldname": "amount", "fieldtype": "Currency", - "label": "Amount" + "label": "Amount", + "read_only": 1 }, { "fieldname": "currency", "fieldtype": "Link", "label": "Currency", - "options": "Currency" + "options": "Currency", + "read_only": 1 }, { "fieldname": "event", "fieldtype": "Data", - "label": "Event" + "label": "Event", + "read_only": 1 }, { "fieldname": "company", "fieldtype": "Link", "label": "Company", - "options": "Company" + "options": "Company", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-10-16 17:08:09.334330", + "modified": "2024-10-16 17:11:28.143979", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Payment Ledger Entry", From bf0b74bcbdb396ac5d1438034b4beb11f796cf78 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 16 Oct 2024 17:16:31 +0530 Subject: [PATCH 05/39] refactor: create advance ledger entries on submit and cancel (cherry picked from commit 575ca5b90048ea96bbb59c4e0d968349a29211aa) --- .../doctype/payment_entry/payment_entry.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b645d92cb64..28ad4454596 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -110,6 +110,7 @@ class PaymentEntry(AccountsController): self.update_outstanding_amounts() self.update_payment_schedule() self.update_payment_requests() + self.make_advance_payment_ledger_entries() self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() @@ -197,6 +198,7 @@ class PaymentEntry(AccountsController): self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) self.update_payment_requests(cancel=True) + self.make_advance_payment_ledger_entries() self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() @@ -1849,6 +1851,26 @@ class PaymentEntry(AccountsController): allocated_negative_outstanding, ) + def make_advance_payment_ledger_entries(self): + if self.docstatus == 1 or self.docstatus == 2: + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_receivable_doctypes" + ) + frappe.get_hooks("advance_payment_payable_doctypes") + + advance_doctype_references = [ + x for x in self.references if x.reference_doctype in advance_payment_doctypes + ] + for x in advance_doctype_references: + doc = frappe.new_doc("Advance Payment Ledger Entry") + doc.company = self.company + doc.voucher_type = self.doctype + doc.voucher_no = self.name + doc.against_voucher_type = x.reference_doctype + doc.against_voucher_no = x.reference_name + doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount + doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.save() + @frappe.whitelist() def set_matched_payment_requests(self, matched_payment_requests): """ From 54f758c327d4e852ae10fc20570ddc59671efeae Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 16 Oct 2024 17:21:56 +0530 Subject: [PATCH 06/39] refactor: calculate advance from advance ledger (cherry picked from commit 2b2360bf7bedb771ca90d0a0a99f90eae693e179) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/controllers/accounts_controller.py | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e6fb5d0d153..22d59f428f2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1935,22 +1935,33 @@ class AccountsController(TransactionBase): return stock_items +<<<<<<< HEAD def set_total_advance_paid(self): ple = frappe.qb.DocType("Payment Ledger Entry") party = self.customer if self.doctype == "Sales Order" else self.supplier +======= + def calculate_total_advance_from_ledger(self): + adv = frappe.qb.DocType("Advance Payment Ledger Entry") +>>>>>>> 2b2360bf7b (refactor: calculate advance from advance ledger) advance = ( - frappe.qb.from_(ple) - .select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount")) + frappe.qb.from_(adv) + .select(adv.currency, Abs(Sum(adv.amount)).as_("amount")) .where( - (ple.against_voucher_type == self.doctype) - & (ple.against_voucher_no == self.name) - & (ple.party == party) - & (ple.delinked == 0) - & (ple.company == self.company) + (adv.against_voucher_type == self.doctype) + & (adv.against_voucher_no == self.name) + & (adv.company == self.company) ) .run(as_dict=True) ) + return advance +<<<<<<< HEAD +======= + def set_total_advance_paid(self): + advance = self.calculate_total_advance_from_ledger() + advance_paid, order_total = None, None + +>>>>>>> 2b2360bf7b (refactor: calculate advance from advance ledger) if advance: advance = advance[0] From a6c26874c784b7ee5abea4916ee99f7344f15ab5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 16 Oct 2024 17:26:20 +0530 Subject: [PATCH 07/39] refactor: remove advance payment ledgers on document deletion (cherry picked from commit 3c53b92f0561907ed71a41d82403ca77a088d3ed) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 1 + erpnext/controllers/accounts_controller.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 28ad4454596..abd6ea37e6d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -191,6 +191,7 @@ class PaymentEntry(AccountsController): "Repost Accounting Ledger Items", "Unreconcile Payment", "Unreconcile Payment Entries", + "Advance Payment Ledger Entry", ) super().on_cancel() self.make_gl_entries(cancel=1) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 22d59f428f2..ffe28119041 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -345,9 +345,14 @@ class AccountsController(TransactionBase): repost_doc.flags.ignore_links = True repost_doc.save(ignore_permissions=True) + def _remove_advance_payment_ledger_entries(self): + adv = qb.DocType("Advance Payment Ledger Entry") + qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run() + def on_trash(self): from erpnext.accounts.utils import delete_exchange_gain_loss_journal + self._remove_advance_payment_ledger_entries() self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() self.remove_serial_and_batch_bundle() From df25d33735ad79dca49c17a0daae8b0cc6fe39e7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 17 Oct 2024 13:49:46 +0530 Subject: [PATCH 08/39] chore: remove duplicate test class (cherry picked from commit e2891a60d5a48205c544585032a0dae78975ac45) --- .../test_advance_payment_ledger_entry.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py index ef938062399..750a658102d 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -11,15 +11,6 @@ EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -class TestAdvancePaymentLedgerEntry(UnitTestCase): - """ - Unit tests for AdvancePaymentLedgerEntry. - Use this class for testing individual functions and methods. - """ - - pass - - class TestAdvancePaymentLedgerEntry(IntegrationTestCase): """ Integration tests for AdvancePaymentLedgerEntry. From a12df122a96d0e0313a475bec2b503707767b106 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 17 Oct 2024 13:56:24 +0530 Subject: [PATCH 09/39] refactor(test): advance_paid stays after reconciliation (cherry picked from commit c4197c3f31336ebca8ec2b81ee882ff2a43baccf) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 7ac0d34e671..d05da0dbf19 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1995,7 +1995,7 @@ class TestSalesInvoice(FrappeTestCase): # Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0 self.assertEqual(pe.references[0].reference_name, si.name) - self.assertEqual(sales_order.advance_paid, 0.0) + self.assertEqual(sales_order.advance_paid, 300.0) # check outstanding after advance allocation self.assertEqual( From d84a3c4f29df46156d94cef8ad195eaf59424e06 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 17 Oct 2024 14:31:18 +0530 Subject: [PATCH 10/39] fix: deleting SO/PO will remove its advance payment ledger entry (cherry picked from commit 14357bccba2c5a6e21091fc99239b91dcc7b647f) --- erpnext/controllers/accounts_controller.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ffe28119041..23498ad6883 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -349,6 +349,14 @@ class AccountsController(TransactionBase): adv = qb.DocType("Advance Payment Ledger Entry") qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run() + advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( + "advance_payment_payable_doctypes" + ) + if self.doctype in advance_payment_doctypes: + qb.from_(adv).delete().where( + adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name) + ).run() + def on_trash(self): from erpnext.accounts.utils import delete_exchange_gain_loss_journal From ffd426d43db54349cd4c969c66fee84e5ad54301 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 17 Oct 2024 16:51:16 +0530 Subject: [PATCH 11/39] refactor: link journal entry to advance payment ledger (cherry picked from commit fca5e952484af634f0f1268258dd0bf8c56bb73a) --- .../doctype/journal_entry/journal_entry.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 47626492e84..d11cc27ccb7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -188,6 +188,7 @@ class JournalEntry(AccountsController): self.validate_cheque_info() self.check_credit_limit() self.make_gl_entries() + self.make_advance_payment_ledger_entries() self.update_advance_paid() self.update_asset_value() self.update_inter_company_jv() @@ -220,6 +221,7 @@ class JournalEntry(AccountsController): "Unreconcile Payment Entries", ) self.make_gl_entries(1) + self.make_advance_payment_ledger_entries() self.update_advance_paid() self.unlink_advance_entry_reference() self.unlink_asset_reference() @@ -231,6 +233,32 @@ class JournalEntry(AccountsController): def get_title(self): return self.pay_to_recd_from or self.accounts[0].account + def make_advance_payment_ledger_entries(self): + if self.docstatus == 1 or self.docstatus == 2: + advance_payment_doctypes = frappe.get_hooks( + "advance_payment_receivable_doctypes" + ) + frappe.get_hooks("advance_payment_payable_doctypes") + + advance_doctype_references = [ + x for x in self.accounts if x.reference_type in advance_payment_doctypes + ] + + for x in advance_doctype_references: + # Looking for payments + dr_or_cr = "credit" if x.account_type == "Receivable" else "debit" + + amount = x.get(dr_or_cr) + if amount > 0: + doc = frappe.new_doc("Advance Payment Ledger Entry") + doc.company = self.company + doc.voucher_type = self.doctype + doc.voucher_no = self.name + doc.against_voucher_type = x.reference_type + doc.against_voucher_no = x.reference_name + doc.amount = amount if self.docstatus == 1 else -1 * amount + doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.save() + def update_advance_paid(self): advance_paid = frappe._dict() for d in self.get("accounts"): From cb36dcb3823a3d50907e3084819db33271a52345 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Oct 2024 13:32:28 +0530 Subject: [PATCH 12/39] refactor(test): reconciliation shouldn't affect advance paid (cherry picked from commit 35a8a187283efe4a9ce325017278d1e3b0bbdfbc) --- .../unreconcile_payment/test_unreconcile_payment.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 13e5294aa78..3d222b22ff8 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -362,10 +362,14 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): # Assert 'Advance Paid' so.reload() pe.reload() - self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.advance_paid, 100) self.assertEqual(len(pe.references), 0) self.assertEqual(pe.unallocated_amount, 100) + pe.cancel() + so.reload() + self.assertEqual(so.advance_paid, 100) + def test_06_unreconcile_advance_from_payment_entry(self): self.enable_advance_as_liability() so1 = self.create_sales_order() @@ -411,7 +415,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): so2.reload() pe.reload() self.assertEqual(so1.advance_paid, 150) - self.assertEqual(so2.advance_paid, 0) + self.assertEqual(so2.advance_paid, 110) self.assertEqual(len(pe.references), 1) self.assertEqual(pe.unallocated_amount, 110) @@ -459,6 +463,6 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): # Assert 'Advance Paid' so.reload() - self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.advance_paid, 1000) self.disable_advance_as_liability() From 085e0455d80a853dddb01fe34123f613b51b1693 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Oct 2024 13:48:15 +0530 Subject: [PATCH 13/39] refactor: patch to migrating old SO / PO to advance ledger (cherry picked from commit b927f2f4a0744db8680dcd9d664ec4c9ab9453f0) --- .../create_advance_payment_ledger_records.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 erpnext/patches/v15_0/create_advance_payment_ledger_records.py diff --git a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py new file mode 100644 index 00000000000..64d8e3c0f3c --- /dev/null +++ b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py @@ -0,0 +1,71 @@ +import frappe +from frappe import qb +from frappe.query_builder.custom import ConstantColumn + + +def get_advance_doctypes() -> list: + return frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( + "advance_payment_payable_doctypes" + ) + + +def get_payments_with_so_po_reference() -> list: + advance_doctypes = get_advance_doctypes() + per = qb.DocType("Payment Entry Reference") + payments_with_reference = ( + qb.from_(per) + .select(per.parent) + .distinct() + .where(per.reference_doctype.isin(advance_doctypes) & per.docstatus.eq(1)) + .run() + ) + pe = qb.DocType("Payment Entry") + advance_payment_entries = ( + qb.from_(pe) + .select(ConstantColumn("Payment Entry").as_("doctype")) + .select(pe.name) + .where(pe.name.isin(payments_with_reference) & pe.docstatus.eq(1)) + .run(as_dict=True) + ) + + return advance_payment_entries + + +def get_journals_with_so_po_reference() -> list: + advance_doctypes = get_advance_doctypes() + jea = qb.DocType("Journal Entry Account") + journals_with_reference = ( + qb.from_(jea) + .select(jea.parent) + .distinct() + .where(jea.reference_type.isin(advance_doctypes) & jea.docstatus.eq(1)) + .run() + ) + je = qb.DocType("Journal Entry") + advance_payment_entries = ( + qb.from_(je) + .select(ConstantColumn("Journal Entry").as_("doctype")) + .select(je.name) + .where(je.name.isin(journals_with_reference) & je.docstatus.eq(1)) + .run(as_dict=True) + ) + + return advance_payment_entries + + +def make_advance_ledger_entries(vouchers: list): + for x in vouchers: + frappe.get_doc(x.doctype, x.name).make_advance_payment_ledger_entries() + + +def execute(): + """ + Description: + Create Advance Payment Ledger Entry for all Payments made against Sales / Purchase Orders + """ + frappe.db.truncate("Advance Payment Ledger Entry") + payment_entries = get_payments_with_so_po_reference() + make_advance_ledger_entries(payment_entries) + + journals = get_journals_with_so_po_reference() + make_advance_ledger_entries(journals) From c6bfdcf50319486ab7348420667b5d4060e666c8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Oct 2024 16:22:38 +0530 Subject: [PATCH 14/39] chore: update patchex.txt (cherry picked from commit 8ab7194b1d6112af4ddb8034fb34dd9d49cf623f) --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f48dc00fc8e..64c74f9d645 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -359,6 +359,7 @@ erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16 +erpnext.patches.v15_0.create_advance_payment_ledger_records # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 From 063cef576cbebb8fb69b980a14d39b42caada964 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Oct 2024 16:25:58 +0530 Subject: [PATCH 15/39] chore: update ignore_linked_doctypes for Journal Entry (cherry picked from commit 767ae6a372969ecbd03e7b66af3d9ede4202e9b0) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d11cc27ccb7..bd25b13e999 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -219,6 +219,7 @@ class JournalEntry(AccountsController): "Repost Accounting Ledger Items", "Unreconcile Payment", "Unreconcile Payment Entries", + "Advance Payment Ledger Entry", ) self.make_gl_entries(1) self.make_advance_payment_ledger_entries() From 164d7cc896138b1c7bbfbdf00efe39fdb71c9dbb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Oct 2024 16:07:14 +0530 Subject: [PATCH 16/39] refactor: handle 'no data' situation in patch (cherry picked from commit 8e3bf7dc09dfdbe51043179dd32c04ab1f61f23e) --- .../create_advance_payment_ledger_records.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py index 64d8e3c0f3c..13b4d95c760 100644 --- a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py +++ b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py @@ -10,6 +10,7 @@ def get_advance_doctypes() -> list: def get_payments_with_so_po_reference() -> list: + advance_payment_entries = [] advance_doctypes = get_advance_doctypes() per = qb.DocType("Payment Entry Reference") payments_with_reference = ( @@ -19,19 +20,21 @@ def get_payments_with_so_po_reference() -> list: .where(per.reference_doctype.isin(advance_doctypes) & per.docstatus.eq(1)) .run() ) - pe = qb.DocType("Payment Entry") - advance_payment_entries = ( - qb.from_(pe) - .select(ConstantColumn("Payment Entry").as_("doctype")) - .select(pe.name) - .where(pe.name.isin(payments_with_reference) & pe.docstatus.eq(1)) - .run(as_dict=True) - ) + if payments_with_reference: + pe = qb.DocType("Payment Entry") + advance_payment_entries = ( + qb.from_(pe) + .select(ConstantColumn("Payment Entry").as_("doctype")) + .select(pe.name) + .where(pe.name.isin(payments_with_reference) & pe.docstatus.eq(1)) + .run(as_dict=True) + ) return advance_payment_entries def get_journals_with_so_po_reference() -> list: + advance_journal_entries = [] advance_doctypes = get_advance_doctypes() jea = qb.DocType("Journal Entry Account") journals_with_reference = ( @@ -41,16 +44,17 @@ def get_journals_with_so_po_reference() -> list: .where(jea.reference_type.isin(advance_doctypes) & jea.docstatus.eq(1)) .run() ) - je = qb.DocType("Journal Entry") - advance_payment_entries = ( - qb.from_(je) - .select(ConstantColumn("Journal Entry").as_("doctype")) - .select(je.name) - .where(je.name.isin(journals_with_reference) & je.docstatus.eq(1)) - .run(as_dict=True) - ) + if journals_with_reference: + je = qb.DocType("Journal Entry") + advance_journal_entries = ( + qb.from_(je) + .select(ConstantColumn("Journal Entry").as_("doctype")) + .select(je.name) + .where(je.name.isin(journals_with_reference) & je.docstatus.eq(1)) + .run(as_dict=True) + ) - return advance_payment_entries + return advance_journal_entries def make_advance_ledger_entries(vouchers: list): From 68a95c7dbc943933e65ee0d08d30fdf6a2cbd592 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Oct 2024 17:04:35 +0530 Subject: [PATCH 17/39] refactor: move creation logic to controller (cherry picked from commit ad88bde448d96cf50ece9a65fb3110b8bd89257f) --- .../doctype/journal_entry/journal_entry.py | 26 ---------- .../doctype/payment_entry/payment_entry.py | 20 -------- erpnext/controllers/accounts_controller.py | 50 +++++++++++++++++++ 3 files changed, 50 insertions(+), 46 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index bd25b13e999..fb5c563d790 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -234,32 +234,6 @@ class JournalEntry(AccountsController): def get_title(self): return self.pay_to_recd_from or self.accounts[0].account - def make_advance_payment_ledger_entries(self): - if self.docstatus == 1 or self.docstatus == 2: - advance_payment_doctypes = frappe.get_hooks( - "advance_payment_receivable_doctypes" - ) + frappe.get_hooks("advance_payment_payable_doctypes") - - advance_doctype_references = [ - x for x in self.accounts if x.reference_type in advance_payment_doctypes - ] - - for x in advance_doctype_references: - # Looking for payments - dr_or_cr = "credit" if x.account_type == "Receivable" else "debit" - - amount = x.get(dr_or_cr) - if amount > 0: - doc = frappe.new_doc("Advance Payment Ledger Entry") - doc.company = self.company - doc.voucher_type = self.doctype - doc.voucher_no = self.name - doc.against_voucher_type = x.reference_type - doc.against_voucher_no = x.reference_name - doc.amount = amount if self.docstatus == 1 else -1 * amount - doc.event = "Submit" if self.docstatus == 1 else "Cancel" - doc.save() - def update_advance_paid(self): advance_paid = frappe._dict() for d in self.get("accounts"): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index abd6ea37e6d..8832b87eec7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1852,26 +1852,6 @@ class PaymentEntry(AccountsController): allocated_negative_outstanding, ) - def make_advance_payment_ledger_entries(self): - if self.docstatus == 1 or self.docstatus == 2: - advance_payment_doctypes = frappe.get_hooks( - "advance_payment_receivable_doctypes" - ) + frappe.get_hooks("advance_payment_payable_doctypes") - - advance_doctype_references = [ - x for x in self.references if x.reference_doctype in advance_payment_doctypes - ] - for x in advance_doctype_references: - doc = frappe.new_doc("Advance Payment Ledger Entry") - doc.company = self.company - doc.voucher_type = self.doctype - doc.voucher_no = self.name - doc.against_voucher_type = x.reference_doctype - doc.against_voucher_no = x.reference_name - doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount - doc.event = "Submit" if self.docstatus == 1 else "Cancel" - doc.save() - @frappe.whitelist() def set_matched_payment_requests(self, matched_payment_requests): """ diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 23498ad6883..a7a36b7e643 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2570,6 +2570,56 @@ class AccountsController(TransactionBase): repost_ledger.insert() repost_ledger.submit() + def get_advance_payment_doctypes(self) -> list: + return frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( + "advance_payment_payable_doctypes" + ) + + def make_advance_payment_ledger_for_journal(self): + advance_payment_doctypes = self.get_advance_payment_doctypes() + advance_doctype_references = [ + x for x in self.accounts if x.reference_type in advance_payment_doctypes + ] + + for x in advance_doctype_references: + # Looking for payments + dr_or_cr = "credit" if x.account_type == "Receivable" else "debit" + + amount = x.get(dr_or_cr) + if amount > 0: + doc = frappe.new_doc("Advance Payment Ledger Entry") + doc.company = self.company + doc.voucher_type = self.doctype + doc.voucher_no = self.name + doc.against_voucher_type = x.reference_type + doc.against_voucher_no = x.reference_name + doc.amount = amount if self.docstatus == 1 else -1 * amount + doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.save() + + def make_advance_payment_ledger_for_payment(self): + advance_payment_doctypes = self.get_advance_payment_doctypes() + advance_doctype_references = [ + x for x in self.references if x.reference_doctype in advance_payment_doctypes + ] + for x in advance_doctype_references: + doc = frappe.new_doc("Advance Payment Ledger Entry") + doc.company = self.company + doc.voucher_type = self.doctype + doc.voucher_no = self.name + doc.against_voucher_type = x.reference_doctype + doc.against_voucher_no = x.reference_name + doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount + doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.save() + + def make_advance_payment_ledger_entries(self): + if self.docstatus != 0: + if self.doctype == "Journal Entry": + self.make_advance_payment_ledger_for_journal() + elif self.doctype == "Payment Entry": + self.make_advance_payment_ledger_for_payment() + @frappe.whitelist() def get_tax_rate(account_head): From 07a394a1c509b0539041f5fc88f1d000ff749951 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Oct 2024 10:16:47 +0530 Subject: [PATCH 18/39] refactor: handle currency on advance payment ledger (cherry picked from commit ae6a81cd07e5f78a4b6107028a1322ec82978ae9) --- erpnext/controllers/accounts_controller.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a7a36b7e643..6870c06bf5d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1958,7 +1958,7 @@ class AccountsController(TransactionBase): >>>>>>> 2b2360bf7b (refactor: calculate advance from advance ledger) advance = ( frappe.qb.from_(adv) - .select(adv.currency, Abs(Sum(adv.amount)).as_("amount")) + .select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount")) .where( (adv.against_voucher_type == self.doctype) & (adv.against_voucher_no == self.name) @@ -2602,6 +2602,11 @@ class AccountsController(TransactionBase): advance_doctype_references = [ x for x in self.references if x.reference_doctype in advance_payment_doctypes ] + currency = ( + self.paid_from_account_currency + if self.payment_type == "Receive" + else self.paid_to_account_currency + ) for x in advance_doctype_references: doc = frappe.new_doc("Advance Payment Ledger Entry") doc.company = self.company @@ -2610,6 +2615,7 @@ class AccountsController(TransactionBase): doc.against_voucher_type = x.reference_doctype doc.against_voucher_no = x.reference_name doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount + doc.currency = currency doc.event = "Submit" if self.docstatus == 1 else "Cancel" doc.save() From d830ce1d88f7377627f047458f8c07b11b6b3725 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Oct 2024 12:21:41 +0530 Subject: [PATCH 19/39] test: USD Sales Order with advance payment (cherry picked from commit 6c731561f3afb7208a7d8489f25c6d2008d6fd71) --- .../doctype/sales_order/test_sales_order.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 244a6b1ddad..c2c587899d4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1327,6 +1327,64 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.reload() self.assertEqual(so.advance_paid, 0) + def create_foreign_currency_usd_account(self): + account_name = "Debtors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": "_Test Company"} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Receivable - _TC" + acc.company = "_Test Company" + acc.account_currency = "USD" + acc.account_type = "Receivable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": "_Test Company"}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.debtors_usd = acc.name + + def test_advance_paid_and_currency_with_payment(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + + self.create_customer("_Test USD Customer", "USD") + self.create_foreign_currency_usd_account() + + so = make_sales_order(customer=self.customer, currency="USD", qty=1, rate=100, do_not_submit=True) + so.conversion_rate = 80 + so.submit() + + pe_exchange_rate = 85 + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_from = self.debtors_usd + pe.paid_from_account_currency = "USD" + pe.source_exchange_rate = pe_exchange_rate + pe.paid_amount = so.grand_total + pe.received_amount = pe_exchange_rate * pe.paid_amount + pe.references[0].outstanding_amount = 100 + pe.references[0].total_amount = 100 + pe.references[0].allocated_amount = 100 + pe.save().submit() + + so.reload() + self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.party_account_currency, "USD") + + # cancel advance payment + pe.reload() + pe.cancel() + + so.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.party_account_currency, "USD") + def test_cancel_sales_order_after_cancel_payment_entry(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry From c8be4f3f781119516128d113365da736a149ebc1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Oct 2024 12:58:29 +0530 Subject: [PATCH 20/39] refactor: use dr / cr account currency field for journals (cherry picked from commit 9c1a4e284c0cbc0f943920a5501a6503ce4c5fde) --- erpnext/controllers/accounts_controller.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6870c06bf5d..03910516dea 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2583,7 +2583,11 @@ class AccountsController(TransactionBase): for x in advance_doctype_references: # Looking for payments - dr_or_cr = "credit" if x.account_type == "Receivable" else "debit" + dr_or_cr = ( + "credit_in_account_currency" + if x.account_type == "Receivable" + else "debit_in_account_currency" + ) amount = x.get(dr_or_cr) if amount > 0: @@ -2595,6 +2599,7 @@ class AccountsController(TransactionBase): doc.against_voucher_no = x.reference_name doc.amount = amount if self.docstatus == 1 else -1 * amount doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.currency = x.account_currency doc.save() def make_advance_payment_ledger_for_payment(self): From 7f9ae4e0447a2572a9909b88753e9311365ef464 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Oct 2024 13:07:39 +0530 Subject: [PATCH 21/39] test: advance and currency from Journal (cherry picked from commit 18250825126be744b6bf4db3ce6bf23670d0a12a) --- .../doctype/sales_order/test_sales_order.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index c2c587899d4..640669efbb7 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1385,6 +1385,57 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.advance_paid, 0) self.assertEqual(so.party_account_currency, "USD") + def test_advance_paid_and_currency_with_journal(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + + self.create_customer("_Test USD Customer", "USD") + self.create_foreign_currency_usd_account() + + so = make_sales_order(customer=self.customer, currency="USD", qty=1, rate=100, do_not_submit=True) + so.conversion_rate = 80 + so.submit() + + je_exchange_rate = 85 + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": "_Test Company", + "voucher_type": "Journal Entry", + "posting_date": so.transaction_date, + "multi_currency": True, + "accounts": [ + { + "account": self.debtors_usd, + "party_type": "Customer", + "party": so.customer, + "credit": 8500, + "credit_in_account_currency": 100, + "is_advance": "Yes", + "reference_type": so.doctype, + "reference_name": so.name, + "exchange_rate": je_exchange_rate, + }, + { + "account": "_Test Bank - _TC", + "debit": 8500, + "debit_in_account_currency": 8500, + }, + ], + } + ) + je.save().submit() + so.reload() + self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.party_account_currency, "USD") + + # cancel advance payment + je.reload() + je.cancel() + + so.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.party_account_currency, "USD") + def test_cancel_sales_order_after_cancel_payment_entry(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry From 16c1fc75b5683cb75ab482ce63c07ab07e1cfb06 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 31 Oct 2024 10:08:41 +0530 Subject: [PATCH 22/39] chore: move tests to advance payment ledger doctype (cherry picked from commit 14cef3d4c4056bc4840c5a840c43b26fd803b388) --- .../test_advance_payment_ledger_entry.py | 116 +++++++++++++++++- .../doctype/sales_order/test_sales_order.py | 109 ---------------- 2 files changed, 113 insertions(+), 112 deletions(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py index 750a658102d..c953291ac6f 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -1,8 +1,13 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests import IntegrationTestCase, UnitTestCase +from frappe.utils import nowdate, today + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order # On IntegrationTestCase, the doctype test records and all # link-field test record depdendencies are recursively loaded @@ -11,10 +16,115 @@ EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -class TestAdvancePaymentLedgerEntry(IntegrationTestCase): +class TestAdvancePaymentLedgerEntry(AccountsTestMixin, IntegrationTestCase): """ Integration tests for AdvancePaymentLedgerEntry. Use this class for testing interactions between multiple components. """ - pass + def setUp(self): + self.create_company() + self.create_usd_receivable_account() + self.create_usd_payable_account() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False): + """ + Helper method + """ + so = make_sales_order( + company=self.company, + customer=self.customer, + currency=currency, + item=self.item, + qty=qty, + rate=rate, + transaction_date=today(), + do_not_submit=do_not_submit, + ) + return so + + def test_so_advance_paid_and_currency_with_payment(self): + self.create_customer("_Test USD Customer", "USD") + + so = self.create_sales_order(currency="USD", do_not_submit=True) + so.conversion_rate = 80 + so.submit() + + pe_exchange_rate = 85 + pe = get_payment_entry(so.doctype, so.name, bank_account=self.cash) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_from = self.debtors_usd + pe.paid_from_account_currency = "USD" + pe.source_exchange_rate = pe_exchange_rate + pe.paid_amount = so.grand_total + pe.received_amount = pe_exchange_rate * pe.paid_amount + pe.references[0].outstanding_amount = 100 + pe.references[0].total_amount = 100 + pe.references[0].allocated_amount = 100 + pe.save().submit() + + so.reload() + self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.party_account_currency, "USD") + + # cancel advance payment + pe.reload() + pe.cancel() + + so.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.party_account_currency, "USD") + + def test_so_advance_paid_and_currency_with_journal(self): + self.create_customer("_Test USD Customer", "USD") + + so = self.create_sales_order(currency="USD", do_not_submit=True) + so.conversion_rate = 80 + so.submit() + + je_exchange_rate = 85 + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.company, + "voucher_type": "Journal Entry", + "posting_date": so.transaction_date, + "multi_currency": True, + "accounts": [ + { + "account": self.debtors_usd, + "party_type": "Customer", + "party": so.customer, + "credit": 8500, + "credit_in_account_currency": 100, + "is_advance": "Yes", + "reference_type": so.doctype, + "reference_name": so.name, + "exchange_rate": je_exchange_rate, + }, + { + "account": self.cash, + "debit": 8500, + "debit_in_account_currency": 8500, + }, + ], + } + ) + je.save().submit() + so.reload() + self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.party_account_currency, "USD") + + # cancel advance payment + je.reload() + je.cancel() + + so.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.party_account_currency, "USD") diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 640669efbb7..244a6b1ddad 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1327,115 +1327,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.reload() self.assertEqual(so.advance_paid, 0) - def create_foreign_currency_usd_account(self): - account_name = "Debtors USD" - if not frappe.db.get_value( - "Account", filters={"account_name": account_name, "company": "_Test Company"} - ): - acc = frappe.new_doc("Account") - acc.account_name = account_name - acc.parent_account = "Accounts Receivable - _TC" - acc.company = "_Test Company" - acc.account_currency = "USD" - acc.account_type = "Receivable" - acc.insert() - else: - name = frappe.db.get_value( - "Account", - filters={"account_name": account_name, "company": "_Test Company"}, - fieldname="name", - pluck=True, - ) - acc = frappe.get_doc("Account", name) - self.debtors_usd = acc.name - - def test_advance_paid_and_currency_with_payment(self): - from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - - self.create_customer("_Test USD Customer", "USD") - self.create_foreign_currency_usd_account() - - so = make_sales_order(customer=self.customer, currency="USD", qty=1, rate=100, do_not_submit=True) - so.conversion_rate = 80 - so.submit() - - pe_exchange_rate = 85 - pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") - pe.reference_no = "1" - pe.reference_date = nowdate() - pe.paid_from = self.debtors_usd - pe.paid_from_account_currency = "USD" - pe.source_exchange_rate = pe_exchange_rate - pe.paid_amount = so.grand_total - pe.received_amount = pe_exchange_rate * pe.paid_amount - pe.references[0].outstanding_amount = 100 - pe.references[0].total_amount = 100 - pe.references[0].allocated_amount = 100 - pe.save().submit() - - so.reload() - self.assertEqual(so.advance_paid, 100) - self.assertEqual(so.party_account_currency, "USD") - - # cancel advance payment - pe.reload() - pe.cancel() - - so.reload() - self.assertEqual(so.advance_paid, 0) - self.assertEqual(so.party_account_currency, "USD") - - def test_advance_paid_and_currency_with_journal(self): - from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - - self.create_customer("_Test USD Customer", "USD") - self.create_foreign_currency_usd_account() - - so = make_sales_order(customer=self.customer, currency="USD", qty=1, rate=100, do_not_submit=True) - so.conversion_rate = 80 - so.submit() - - je_exchange_rate = 85 - je = frappe.get_doc( - { - "doctype": "Journal Entry", - "company": "_Test Company", - "voucher_type": "Journal Entry", - "posting_date": so.transaction_date, - "multi_currency": True, - "accounts": [ - { - "account": self.debtors_usd, - "party_type": "Customer", - "party": so.customer, - "credit": 8500, - "credit_in_account_currency": 100, - "is_advance": "Yes", - "reference_type": so.doctype, - "reference_name": so.name, - "exchange_rate": je_exchange_rate, - }, - { - "account": "_Test Bank - _TC", - "debit": 8500, - "debit_in_account_currency": 8500, - }, - ], - } - ) - je.save().submit() - so.reload() - self.assertEqual(so.advance_paid, 100) - self.assertEqual(so.party_account_currency, "USD") - - # cancel advance payment - je.reload() - je.cancel() - - so.reload() - self.assertEqual(so.advance_paid, 0) - self.assertEqual(so.party_account_currency, "USD") - def test_cancel_sales_order_after_cancel_payment_entry(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry From 91a276d4edbbd34ac40e63770fd9627791c76761 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 31 Oct 2024 10:24:20 +0530 Subject: [PATCH 23/39] test: PO 'Advance Paid' and curreny when using payment (cherry picked from commit ca85c75e3950d2dd1d7b5cba88ea61809b0e2f2a) --- .../test_advance_payment_ledger_entry.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py index c953291ac6f..4fe206a351b 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -7,6 +7,7 @@ from frappe.utils import nowdate, today from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order # On IntegrationTestCase, the doctype test records and all @@ -48,6 +49,22 @@ class TestAdvancePaymentLedgerEntry(AccountsTestMixin, IntegrationTestCase): ) return so + def create_purchase_order(self, qty=1, rate=100, currency="INR", do_not_submit=False): + """ + Helper method + """ + po = create_purchase_order( + company=self.company, + customer=self.supplier, + currency=currency, + item=self.item, + qty=qty, + rate=rate, + transaction_date=today(), + do_not_submit=do_not_submit, + ) + return po + def test_so_advance_paid_and_currency_with_payment(self): self.create_customer("_Test USD Customer", "USD") @@ -128,3 +145,36 @@ class TestAdvancePaymentLedgerEntry(AccountsTestMixin, IntegrationTestCase): so.reload() self.assertEqual(so.advance_paid, 0) self.assertEqual(so.party_account_currency, "USD") + + def test_po_advance_paid_and_currency_with_payment(self): + self.create_supplier("_Test USD Supplier", "USD") + + po = self.create_purchase_order(currency="USD", do_not_submit=True) + po.conversion_rate = 80 + po.submit() + + pe_exchange_rate = 85 + pe = get_payment_entry(po.doctype, po.name, bank_account=self.cash) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_to = self.creditors_usd + pe.paid_to_account_currency = "USD" + pe.target_exchange_rate = pe_exchange_rate + pe.received_amount = po.grand_total + pe.paid_amount = pe_exchange_rate * pe.received_amount + pe.references[0].outstanding_amount = 100 + pe.references[0].total_amount = 100 + pe.references[0].allocated_amount = 100 + pe.save().submit() + + po.reload() + self.assertEqual(po.advance_paid, 100) + self.assertEqual(po.party_account_currency, "USD") + + # cancel advance payment + pe.reload() + pe.cancel() + + po.reload() + self.assertEqual(po.advance_paid, 0) + self.assertEqual(po.party_account_currency, "USD") From d0a655d5ae15732781f3e1ca824a4374b3295ad9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 31 Oct 2024 10:27:20 +0530 Subject: [PATCH 24/39] test: PO advance and currency from journal (cherry picked from commit cf7b8f1b41c4ff9e86b6cb2eef01e5bd1d49d95e) --- .../test_advance_payment_ledger_entry.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py index 4fe206a351b..f9ac59bfa80 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -178,3 +178,51 @@ class TestAdvancePaymentLedgerEntry(AccountsTestMixin, IntegrationTestCase): po.reload() self.assertEqual(po.advance_paid, 0) self.assertEqual(po.party_account_currency, "USD") + + def test_po_advance_paid_and_currency_with_journal(self): + self.create_supplier("_Test USD Supplier", "USD") + + po = self.create_purchase_order(currency="USD", do_not_submit=True) + po.conversion_rate = 80 + po.submit() + + je_exchange_rate = 85 + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.company, + "voucher_type": "Journal Entry", + "posting_date": po.transaction_date, + "multi_currency": True, + "accounts": [ + { + "account": self.creditors_usd, + "party_type": "Supplier", + "party": po.supplier, + "debit": 8500, + "debit_in_account_currency": 100, + "is_advance": "Yes", + "reference_type": po.doctype, + "reference_name": po.name, + "exchange_rate": je_exchange_rate, + }, + { + "account": self.cash, + "credit": 8500, + "credit_in_account_currency": 8500, + }, + ], + } + ) + je.save().submit() + po.reload() + self.assertEqual(po.advance_paid, 100) + self.assertEqual(po.party_account_currency, "USD") + + # cancel advance payment + je.reload() + je.cancel() + + po.reload() + self.assertEqual(po.advance_paid, 0) + self.assertEqual(po.party_account_currency, "USD") From ba09ddcc3a0d02fc8cb318dd381e11e07afcccf5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Nov 2024 14:10:45 +0530 Subject: [PATCH 25/39] chore: resolve conflict --- erpnext/controllers/accounts_controller.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 03910516dea..d6016ef7d1d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1948,14 +1948,8 @@ class AccountsController(TransactionBase): return stock_items -<<<<<<< HEAD - def set_total_advance_paid(self): - ple = frappe.qb.DocType("Payment Ledger Entry") - party = self.customer if self.doctype == "Sales Order" else self.supplier -======= def calculate_total_advance_from_ledger(self): adv = frappe.qb.DocType("Advance Payment Ledger Entry") ->>>>>>> 2b2360bf7b (refactor: calculate advance from advance ledger) advance = ( frappe.qb.from_(adv) .select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount")) @@ -1968,13 +1962,10 @@ class AccountsController(TransactionBase): ) return advance -<<<<<<< HEAD -======= def set_total_advance_paid(self): advance = self.calculate_total_advance_from_ledger() advance_paid, order_total = None, None ->>>>>>> 2b2360bf7b (refactor: calculate advance from advance ledger) if advance: advance = advance[0] From 426010e21a3a02e2f2711a44f9b3e30bb209e6e2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Nov 2024 14:14:03 +0530 Subject: [PATCH 26/39] refactor: fetch correct hook variable --- erpnext/controllers/accounts_controller.py | 9 +++------ .../v15_0/create_advance_payment_ledger_records.py | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d6016ef7d1d..1ae6b5256a5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -349,9 +349,8 @@ class AccountsController(TransactionBase): adv = qb.DocType("Advance Payment Ledger Entry") qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run() - advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( - "advance_payment_payable_doctypes" - ) + advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes") + if self.doctype in advance_payment_doctypes: qb.from_(adv).delete().where( adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name) @@ -2562,9 +2561,7 @@ class AccountsController(TransactionBase): repost_ledger.submit() def get_advance_payment_doctypes(self) -> list: - return frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( - "advance_payment_payable_doctypes" - ) + return frappe.get_hooks("advance_payment_doctypes") def make_advance_payment_ledger_for_journal(self): advance_payment_doctypes = self.get_advance_payment_doctypes() diff --git a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py index 13b4d95c760..8d247885cab 100644 --- a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py +++ b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py @@ -4,9 +4,7 @@ from frappe.query_builder.custom import ConstantColumn def get_advance_doctypes() -> list: - return frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( - "advance_payment_payable_doctypes" - ) + return frappe.get_hooks("advance_payment_doctypes") def get_payments_with_so_po_reference() -> list: From 9bfcad31fd45d1585830d059bd8126853a1eef12 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Nov 2024 20:08:07 +0530 Subject: [PATCH 27/39] refactor: replace non-existant IntegrationTestCase --- .../test_advance_payment_ledger_entry.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py index f9ac59bfa80..2f578aed172 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -2,7 +2,7 @@ # See license.txt import frappe -from frappe.tests import IntegrationTestCase, UnitTestCase +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate, today from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -10,14 +10,8 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -# On IntegrationTestCase, the doctype test records and all -# link-field test record depdendencies are recursively loaded -# Use these module variables to add/remove to/from that list -EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] -IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - -class TestAdvancePaymentLedgerEntry(AccountsTestMixin, IntegrationTestCase): +class TestAdvancePaymentLedgerEntry(AccountsTestMixin, FrappeTestCase): """ Integration tests for AdvancePaymentLedgerEntry. Use this class for testing interactions between multiple components. From a689830bff72e85c940e4fc3a897edaeaa2b2c38 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 04:20:01 +0530 Subject: [PATCH 28/39] fix: validation trigger (backport #43926) (#43943) fix: validation trigger (#43926) (cherry picked from commit ba9fb4effc32f7e5454b8a0bab664195b6cb2108) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.py | 7 +++++++ erpnext/manufacturing/doctype/work_order/work_order.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 009320c7a18..843528de706 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -968,6 +968,13 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + if not d.workstation and not d.workstation_type: + frappe.throw( + _( + "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" + ).format(d.idx, d.operation) + ) + def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index d13cd27a095..1ebcde75366 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -173,10 +173,16 @@ class WorkOrder(Document): self.get_items_and_operations_from_bom() def validate_workstation_type(self): + if not self.docstatus.is_submitted(): + return + for row in self.operations: if not row.workstation and not row.workstation_type: - msg = f"Row {row.idx}: Workstation or Workstation Type is mandatory for an operation {row.operation}" - frappe.throw(_(msg)) + frappe.throw( + _("Row {0}: Workstation or Workstation Type is mandatory for an operation {1}").format( + row.idx, row.operation + ) + ) def validate_sales_order(self): if self.sales_order: From ce42d847b3c98f29f82ecc165dfa7a371a916399 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 04:20:20 +0530 Subject: [PATCH 29/39] fix: valuation rate for sales / purchase return for serial / batch nos (backport #43925) (#43942) fix: valuation rate for sales / purchase return for serial / batch nos (#43925) (cherry picked from commit 01bb1612da5e28a6b80640db4f0e77107ad00017) Co-authored-by: rohitwaghchaure --- .../controllers/sales_and_purchase_return.py | 37 ++- erpnext/controllers/stock_controller.py | 5 + .../delivery_note/test_delivery_note.py | 262 ++++++++++++++++++ .../purchase_receipt/test_purchase_receipt.py | 228 +++++++++++++++ .../serial_and_batch_bundle.py | 94 ++++++- erpnext/stock/serial_batch_bundle.py | 30 +- 6 files changed, 643 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 6da23834b61..b1ba8e22c2e 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -597,6 +597,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return + if not source_doc.use_serial_batch_fields and source_doc.serial_and_batch_bundle: + target_doc.serial_no = None + target_doc.batch_no = None + if ( (source_doc.serial_no or source_doc.batch_no) and not source_doc.serial_and_batch_bundle @@ -899,6 +903,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): "`tabSerial and Batch Entry`.`serial_no`", "`tabSerial and Batch Entry`.`batch_no`", "`tabSerial and Batch Entry`.`qty`", + "`tabSerial and Batch Entry`.`incoming_rate`", "`tabSerial and Batch Bundle`.`voucher_detail_no`", "`tabSerial and Batch Bundle`.`voucher_type`", "`tabSerial and Batch Bundle`.`voucher_no`", @@ -920,15 +925,23 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): if key not in available_dict: available_dict[key] = frappe._dict( - {"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)} + { + "qty": 0.0, + "serial_nos": defaultdict(float), + "batches": defaultdict(float), + "serial_nos_valuation": defaultdict(float), + "batches_valuation": defaultdict(float), + } ) available_dict[key]["qty"] += row.qty if row.serial_no: available_dict[key]["serial_nos"][row.serial_no] += row.qty + available_dict[key]["serial_nos_valuation"][row.serial_no] = row.incoming_rate elif row.batch_no: available_dict[key]["batches"][row.batch_no] += row.qty + available_dict[key]["batches_valuation"][row.batch_no] = row.incoming_rate return available_dict @@ -964,12 +977,13 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False ) ) else: - fields = [ - "serial_and_batch_bundle", - ] + fields = ["serial_and_batch_bundle"] if is_rejected: - fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"]) + fields.append("rejected_serial_and_batch_bundle") + + if doctype == "Purchase Receipt Item": + fields.append("return_qty_from_rejected_warehouse") del filters["rejected_serial_and_batch_bundle"] data = frappe.get_all( @@ -1003,7 +1017,14 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field warehouse = row.get(warehouse_field) qty = abs(row.get(qty_field)) - filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)}) + filterd_serial_batch = frappe._dict( + { + "serial_nos": [], + "batches": defaultdict(float), + "serial_nos_valuation": data.get("serial_nos_valuation"), + "batches_valuation": data.get("batches_valuation"), + } + ) if data.serial_nos: available_serial_nos = [] @@ -1013,7 +1034,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field if available_serial_nos: if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]: - available_serial_nos = get_available_serial_nos(available_serial_nos) + available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse) if len(available_serial_nos) > qty: filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)]) @@ -1098,6 +1119,8 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f "warehouse": warehouse, "serial_nos": data.get("serial_nos"), "batches": data.get("batches"), + "serial_nos_valuation": data.get("serial_nos_valuation"), + "batches_valuation": data.get("batches_valuation"), "posting_date": parent_doc.posting_date, "posting_time": parent_doc.posting_time, "voucher_type": parent_doc.doctype, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 0714bdd3a63..046a0c7da30 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -334,6 +334,11 @@ class StockController(AccountsController): } ) + if self.doctype in ["Sales Invoice", "Delivery Note"]: + row.db_set( + "incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + ) + def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]: field = { "Sales Invoice": "sales_invoice_item", diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 0cfb427c670..9acdce8bebc 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -3,6 +3,7 @@ import json +from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase @@ -2080,6 +2081,264 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(stock_value_difference, 100.0 * 5) + def test_delivery_note_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TP-.#####"}, + ).name + + batches = {} + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches[get_batch_from_bundle(se.items[0].serial_and_batch_bundle)] = qty + + for qty, rate in {2: 100, 1: 50}.items(): + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=5, + rate=1000, + use_serial_batch_fields=0, + batches=batches, + do_not_submit=True, + ) + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": serial_item, + "warehouse": dn.items[0].warehouse, + "qty": 3, + "voucher_type": "Delivery Note", + "serial_nos": serial_nos, + "posting_date": dn.posting_date, + "posting_time": dn.posting_time, + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + ).name + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 3, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 0, + "serial_and_batch_bundle": bundle_id, + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + + def test_delivery_note_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation WITH Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BIW-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation WITH Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TPW-.#####"}, + ).name + + batches = [] + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle)) + + for qty, rate in {2: 100, 1: 50}.items(): + se = make_stock_entry( + item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=3, + rate=1000, + use_serial_batch_fields=1, + batch_no=batches[0], + do_not_submit=True, + ) + + dn.append( + "items", + { + "item_code": batch_item, + "qty": 2, + "rate": 1000, + "base_rate": 1000, + "item_name": batch_item, + "uom": dn.items[0].uom, + "stock_uom": dn.items[0].uom, + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "batch_no": batches[1], + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": "\n".join(serial_nos[0:2]), + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 1, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": serial_nos[-1], + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -2107,6 +2366,9 @@ def create_delivery_note(**args): if args.get("batch_no"): batches = frappe._dict({args.batch_no: qty}) + if args.get("batches"): + batches = frappe._dict(args.batches) + bundle_id = make_serial_batch_bundle( frappe._dict( { diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 6d0fe27033f..64c3d2c67b2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3672,6 +3672,234 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.items[0].conversion_factor, 1.0) + def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-TPRBI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-TPRSI-.#####"}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + rate=300, + do_not_submit=1, + use_serial_batch_fields=0, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 0, + "rejected_warehouse": rej_warehouse, + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + + def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation With Batch Item", + properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1}, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation With Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + batch_no = "BATCH-RTN-BNU-TPRBI-0001" + serial_nos = ["SNU-RTN-TPRSI-0001", "SNU-RTN-TPRSI-0002", "SNU-RTN-TPRSI-0003"] + + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": batch_item, + } + ).insert() + + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "item_code": serial_item, + "serial_no": serial_no, + } + ).insert() + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + rate=300, + do_not_submit=1, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 1, + "rejected_warehouse": rej_warehouse, + "serial_no": "\n".join(serial_nos[:2]), + "rejected_serial_no": serial_nos[2], + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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 94ec8675db8..ed6d5e155d7 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 @@ -197,7 +197,7 @@ class SerialandBatchBundle(Document): def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) - def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False): + def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False): if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [ "Installation Note", "Job Card", @@ -206,13 +206,70 @@ class SerialandBatchBundle(Document): ]: return - if self.type_of_transaction == "Outward": + if return_aginst := self.get_return_aginst(parent=parent): + self.set_valuation_rate_for_return_entry(return_aginst, save) + elif self.type_of_transaction == "Outward": self.set_incoming_rate_for_outward_transaction( row, save, allow_negative_stock=allow_negative_stock ) else: self.set_incoming_rate_for_inward_transaction(row, save) + def set_valuation_rate_for_return_entry(self, return_aginst, save=False): + if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst): + for row in self.entries: + if row.serial_no: + valuation_rate = valuation_details["serial_nos"].get(row.serial_no) + else: + valuation_rate = valuation_details["batches"].get(row.batch_no) + + row.incoming_rate = valuation_rate + row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate) + + if save: + row.db_set( + { + "incoming_rate": row.incoming_rate, + "stock_value_difference": row.stock_value_difference, + } + ) + + def get_valuation_rate_for_return_entry(self, return_aginst): + valuation_details = frappe._dict( + { + "serial_nos": defaultdict(float), + "batches": defaultdict(float), + } + ) + + bundle_data = frappe.get_all( + "Serial and Batch Bundle", + fields=[ + "`tabSerial and Batch Entry`.`serial_no`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`incoming_rate`", + ], + filters=[ + ["Serial and Batch Bundle", "voucher_no", "=", return_aginst], + ["Serial and Batch Entry", "docstatus", "=", 1], + ["Serial and Batch Bundle", "is_cancelled", "=", 0], + ["Serial and Batch Bundle", "item_code", "=", self.item_code], + ["Serial and Batch Bundle", "warehouse", "=", self.warehouse], + ], + order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`", + ) + + if not bundle_data: + return {} + + for row in bundle_data: + if row.serial_no: + valuation_details["serial_nos"][row.serial_no] = row.incoming_rate + else: + valuation_details["batches"][row.batch_no] = row.incoming_rate + + return valuation_details + def calculate_total_qty(self, save=True): self.total_qty = 0.0 for d in self.entries: @@ -327,6 +384,33 @@ class SerialandBatchBundle(Document): return sle + def get_return_aginst(self, parent=None): + return_aginst = None + + if parent and parent.get("is_return") and parent.get("return_against"): + return parent.get("return_against") + + if ( + self.voucher_type + in [ + "Delivery Note", + "Sales Invoice", + "Purchase Invoice", + "Purchase Receipt", + "POS Invoice", + "Subcontracting Receipt", + ] + and self.voucher_type + and self.voucher_no + ): + voucher_details = frappe.db.get_value( + self.voucher_type, self.voucher_no, ["is_return", "return_against"], as_dict=True + ) + if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"): + return voucher_details.get("return_against") + + return return_aginst + def set_incoming_rate_for_inward_transaction(self, row=None, save=False): valuation_field = "valuation_rate" if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]: @@ -354,7 +438,9 @@ class SerialandBatchBundle(Document): rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) for d in self.entries: - if (d.incoming_rate == rate) and d.qty and d.stock_value_difference: + if self.is_rejected: + rate = 0.0 + elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference: continue d.incoming_rate = flt(rate, precision) @@ -403,7 +489,7 @@ class SerialandBatchBundle(Document): # If user has changed the rate in the child table if self.docstatus == 0: - self.set_incoming_rate(save=True, row=row) + self.set_incoming_rate(parent=parent, row=row, save=True) if self.docstatus == 0 and parent.get("is_return") and parent.is_new(): self.reset_qty(row, qty_field=qty_field) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 46724be5927..47521475d0a 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1088,6 +1088,8 @@ class SerialBatchCreation: frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) def set_serial_batch_entries(self, doc): + incoming_rate = self.get("incoming_rate") + if self.get("serial_nos"): serial_no_wise_batch = frappe._dict({}) if self.has_batch_no: @@ -1095,30 +1097,54 @@ class SerialBatchCreation: qty = -1 if self.type_of_transaction == "Outward" else 1 for serial_no in self.serial_nos: + if self.get("serial_nos_valuation"): + incoming_rate = self.get("serial_nos_valuation").get(serial_no) + doc.append( "entries", { "serial_no": serial_no, "qty": qty, "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"), - "incoming_rate": self.get("incoming_rate"), + "incoming_rate": incoming_rate, }, ) elif self.get("batches"): for batch_no, batch_qty in self.batches.items(): + if self.get("batches_valuation"): + incoming_rate = self.get("batches_valuation").get(batch_no) + doc.append( "entries", { "batch_no": batch_no, "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1), - "incoming_rate": self.get("incoming_rate"), + "incoming_rate": incoming_rate, }, ) def create_batch(self): from erpnext.stock.doctype.batch.batch import make_batch + if self.is_rejected: + bundle = frappe.db.get_value( + "Serial and Batch Bundle", + { + "voucher_no": self.voucher_no, + "voucher_type": self.voucher_type, + "voucher_detail_no": self.voucher_detail_no, + "is_rejected": 0, + "docstatus": 1, + "is_cancelled": 0, + }, + "name", + ) + + if bundle: + if batch_no := frappe.db.get_value("Serial and Batch Entry", {"parent": bundle}, "batch_no"): + return batch_no + return make_batch( frappe._dict( { From 9a2b0a965d40e54ff5e26121d212e55ff4a483d4 Mon Sep 17 00:00:00 2001 From: hyaray Date: Fri, 25 Oct 2024 20:22:04 +0800 Subject: [PATCH 30/39] refactor: use year current year start date as default (cherry picked from commit d54283ded578264b3ce9556ad2b7f525f25bbf9c) --- erpnext/accounts/doctype/fiscal_year/fiscal_year.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js index a44b52f08f8..aeb9f982b4d 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js @@ -4,10 +4,7 @@ frappe.ui.form.on("Fiscal Year", { onload: function (frm) { if (frm.doc.__islocal) { - frm.set_value( - "year_start_date", - frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1) - ); + frm.set_value("year_start_date", frappe.datetime.year_start()); } }, year_start_date: function (frm) { From 10d8cc9d66054864afd232ac7377eed4c6f1272a Mon Sep 17 00:00:00 2001 From: ramyasusee Date: Wed, 30 Oct 2024 17:40:11 +0530 Subject: [PATCH 31/39] fix: map reference number while reversing journal (cherry picked from commit 77de783cd414eb2fa23c80668ae10efb87ccfd92) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index fb5c563d790..aeaadae0b30 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1671,6 +1671,8 @@ def make_reverse_journal_entry(source_name, target_doc=None): "debit": "credit", "credit_in_account_currency": "debit_in_account_currency", "credit": "debit", + "reference_type": "reference_type", + "reference_name": "reference_name", }, }, }, From 7ad664d89a3383f2005d938cefffb0a798b204a2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:23:44 +0530 Subject: [PATCH 32/39] perf: avoid reposting of entries created after stock reco (backport #43950) (#43961) perf: avoid reposting of entries created after stock reco (#43950) (cherry picked from commit 7cfe1c8d59360dd9887e83991077326b62a9d895) Co-authored-by: rohitwaghchaure --- .../test_stock_reconciliation.py | 54 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 12 +++++ 2 files changed, 66 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 9bb6ba9ec90..9754443d4ac 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1275,6 +1275,60 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): qty = get_batch_qty(batch_id, warehouse, batch_item_code) self.assertEqual(qty, 110) + def test_skip_reposting_for_entries_after_stock_reco(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = create_item("Test Item For Skip Reposting After Stock Reco", is_stock_item=1).name + + warehouse = "_Test Warehouse - _TC" + + make_stock_entry( + posting_date="2024-11-01", + posting_time="11:00", + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + ) + + create_stock_reconciliation( + posting_date="2024-11-02", + posting_time="11:00", + item_code=item_code, + warehouse=warehouse, + qty=20, + rate=100, + ) + + se = make_stock_entry( + posting_date="2024-11-03", + posting_time="11:00", + item_code=item_code, + source=warehouse, + qty=15, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0}, "stock_value_difference" + ) + + self.assertEqual(stock_value_difference, 1500.00 * -1) + + make_stock_entry( + posting_date="2024-10-29", + posting_time="11:00", + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0}, "stock_value_difference" + ) + + self.assertEqual(stock_value_difference, 1500.00 * -1) + 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 0f058d0a64b..fa53e6b47a9 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -623,9 +623,21 @@ class update_entries_after: if sle.dependant_sle_voucher_detail_no: entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle) + if self.has_stock_reco_with_serial_batch(sle): + break + if self.exceptions: self.raise_exceptions() + def has_stock_reco_with_serial_batch(self, sle): + if ( + sle.vocher_type == "Stock Reconciliation" + and frappe.db.get_value(sle.voucher_type, sle.voucher_no, "set_posting_time") == 1 + ): + return not (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle) + + return False + def process_sle_against_current_timestamp(self): sl_entries = self.get_sle_against_current_voucher() for sle in sl_entries: From 9f7afda4db26e9695c8ab45433d1c0885f076417 Mon Sep 17 00:00:00 2001 From: CaseSolved Date: Wed, 25 Sep 2024 19:09:05 +0100 Subject: [PATCH 33/39] fix: SO link on PO and add in missing dashboard references on both (cherry picked from commit 2017fd80d1bf930fd5185ddb3ddb427eb73841f8) --- .../purchase_order_dashboard.py | 21 +++++++++++++++---- .../sales_order/sales_order_dashboard.py | 4 +++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 36fe079fc98..6ae50e71416 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -14,18 +14,31 @@ def get_data(): "Material Request": ["items", "material_request"], "Supplier Quotation": ["items", "supplier_quotation"], "Project": ["items", "project"], + "Sales Order": ["items", "sales_order"], + "BOM": ["items", "bom"], + "Production Plan": ["items", "production_plan"], + "Blanket Order": ["items", "blanket_order"], }, "transactions": [ - {"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice"]}, - {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry", "Payment Request"]}, + { + "label": _("Related"), + "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"] + }, + { + "label": _("Payment"), + "items": ["Payment Entry", "Journal Entry", "Payment Request"] + }, { "label": _("Reference"), - "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], + "items": ["Supplier Quotation", "Project", "Auto Repeat"], + }, + { + "label": _("Manufacturing"), + "items": ["Material Request", "BOM", "Production Plan", "Blanket Order"], }, { "label": _("Sub-contracting"), "items": ["Subcontracting Order", "Subcontracting Receipt", "Stock Entry"], }, - {"label": _("Internal"), "items": ["Sales Order"]}, ], } diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index c84009725b8..7c1c0deb33f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -15,6 +15,8 @@ def get_data(): }, "internal_links": { "Quotation": ["items", "prevdoc_docname"], + "BOM": ["items", "bom_no"], + "Blanket Order": ["items", "blanket_order"], }, "transactions": [ { @@ -23,7 +25,7 @@ def get_data(): }, {"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]}, {"label": _("Projects"), "items": ["Project"]}, - {"label": _("Manufacturing"), "items": ["Work Order"]}, + {"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]}, {"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]}, {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]}, ], From 84a40c282bc0f5833cb7a063e8f69a023bdf4f51 Mon Sep 17 00:00:00 2001 From: CaseSolved Date: Wed, 25 Sep 2024 19:27:21 +0100 Subject: [PATCH 34/39] chore: linting (cherry picked from commit be6970c850d3c81f1b32ae00ede2e49c588ab2e9) --- .../doctype/purchase_order/purchase_order_dashboard.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 6ae50e71416..3fb8b30f139 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -20,14 +20,8 @@ def get_data(): "Blanket Order": ["items", "blanket_order"], }, "transactions": [ - { - "label": _("Related"), - "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"] - }, - { - "label": _("Payment"), - "items": ["Payment Entry", "Journal Entry", "Payment Request"] - }, + {"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"]}, + {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry", "Payment Request"]}, { "label": _("Reference"), "items": ["Supplier Quotation", "Project", "Auto Repeat"], From 1b8bd0e1f3b496682ce3bb3c1716889a5b805ade Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Nov 2024 10:25:32 +0530 Subject: [PATCH 35/39] refactor: avoid permission issue for non-admin (cherry picked from commit c832d9fb9a7378a7a7186bf9bd9661e92dcfdf9b) --- erpnext/controllers/accounts_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1ae6b5256a5..e47e9917149 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2588,6 +2588,7 @@ class AccountsController(TransactionBase): doc.amount = amount if self.docstatus == 1 else -1 * amount doc.event = "Submit" if self.docstatus == 1 else "Cancel" doc.currency = x.account_currency + doc.flags.ignore_permissions = 1 doc.save() def make_advance_payment_ledger_for_payment(self): @@ -2610,6 +2611,7 @@ class AccountsController(TransactionBase): doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount doc.currency = currency doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.flags.ignore_permissions = 1 doc.save() def make_advance_payment_ledger_entries(self): From eaf6d0d7d88c930fdb7cf42ac938f93a80b6e7db Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Nov 2024 10:31:45 +0530 Subject: [PATCH 36/39] refactor: update advance ledger role requirement (cherry picked from commit e41560d30ba0fe72b53cf41cf5fd34bc3a8a541b) --- .../advance_payment_ledger_entry.json | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json index 1d0a5b42a31..290ed11c98e 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -70,25 +70,41 @@ "read_only": 1 } ], + "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-10-16 17:11:28.143979", + "modified": "2024-11-05 10:31:28.736671", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Payment Ledger Entry", "owner": "Administrator", "permissions": [ { - "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, "read": 1, "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Auditor", + "share": 1 } ], "sort_field": "creation", From b665e4e24ad9105d82af30b7190a50e720c8636f Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:36:26 +0530 Subject: [PATCH 37/39] fix: add precision validation (cherry picked from commit 7daadcf42037db5a5f2d387bb59c7b3936e4f0a3) --- erpnext/assets/doctype/asset/asset.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6bb3ff255cf..c788d5265a7 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -119,6 +119,7 @@ class Asset(AccountsController): # end: auto-generated types def validate(self): + self.validate_precision() self.validate_asset_values() self.validate_asset_and_reference() self.validate_item() @@ -306,6 +307,15 @@ class Asset(AccountsController): title=_("Missing Finance Book"), ) + def validate_precision(self): + float_precision = cint(frappe.db.get_default("float_precision")) or 2 + if self.gross_purchase_amount: + self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision) + if self.opening_accumulated_depreciation: + self.opening_accumulated_depreciation = flt( + self.opening_accumulated_depreciation, float_precision + ) + def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -471,6 +481,9 @@ class Asset(AccountsController): def validate_expected_value_after_useful_life(self): for row in self.get("finance_books"): + row.expected_value_after_useful_life = flt( + row.expected_value_after_useful_life, self.precision("gross_purchase_amount") + ) depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book) if not depr_schedule: From a38819cbd5cef2cc8615cdbd34f5d735378d2dd1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:05:44 +0530 Subject: [PATCH 38/39] perf: too many writes error during reposting (backport #43978) (#43983) perf: too many writes error during reposting (#43978) perf: too many writes error (cherry picked from commit 134c24b9c5218161c239be39aaeec55d06028ea9) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fa53e6b47a9..ccf7c7643c8 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1047,7 +1047,7 @@ class update_entries_after: rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": - self.recalculate_amounts_in_stock_entry(sle.voucher_no) + self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ( @@ -1176,14 +1176,15 @@ class update_entries_after: # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount if not sle.dependant_sle_voucher_detail_no: - self.recalculate_amounts_in_stock_entry(sle.voucher_no) + self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no) - def recalculate_amounts_in_stock_entry(self, voucher_no): + def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no): stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: - d.db_update() + if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse): + d.db_update() def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate): # Update item's incoming rate on transaction From ceccd8c2dcafb743169ce5b161aef3b822d007db Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:30:06 +0530 Subject: [PATCH 39/39] chore: update serial_batch_bundle.py (backport #43981) (#43984) chore: update serial_batch_bundle.py (#43981) Avaliable -> Available (cherry picked from commit 30954ed645ed4174ff6ad08c3bb2d59c05df4a73) Co-authored-by: Ikko Eltociear Ashimine --- erpnext/stock/serial_batch_bundle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 47521475d0a..c1002095b62 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -984,7 +984,7 @@ class SerialBatchCreation: required_qty = flt(abs(self.actual_qty), precision) if required_qty - total_qty > 0: - msg = f"For the item {bold(doc.item_code)}, the Avaliable qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse." + msg = f"For the item {bold(doc.item_code)}, the Available qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse." frappe.throw(msg, title=_("Insufficient Stock")) def set_auto_serial_batch_entries_for_outward(self):