From 22138867f5906dc8808890d78ab7071bc59e92bc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:48:58 +0530 Subject: [PATCH 01/22] fix: set rate for PO created against BO (backport #39765) (#39767) * fix: set rate for PO created against BO (cherry picked from commit 0e5b4e5f07d15fe04855f1c836c5412d3644035a) # Conflicts: # erpnext/manufacturing/doctype/blanket_order/blanket_order.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- erpnext/manufacturing/doctype/blanket_order/blanket_order.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index b5ab63efd0..f8f336a6e8 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -90,6 +90,7 @@ def make_order(source_name): def update_item(source, target, source_parent): target_qty = source.get("qty") - source.get("ordered_qty") target.qty = target_qty if not flt(target_qty) < 0 else 0 + target.rate = source.get("rate") item = get_item_defaults(target.item_code, source_parent.company) if item: target.item_name = item.get("item_name") @@ -111,6 +112,10 @@ def make_order(source_name): }, }, ) + + if target_doc.doctype == "Purchase Order": + target_doc.set_missing_values() + return target_doc From 3c6114ab72d21f3c27eb01baf65002d36b3e25fa Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 7 Feb 2024 18:27:08 +0530 Subject: [PATCH 02/22] fix: remove duplicates from tax category map --- .../tax_withholding_details.py | 10 +-- .../test_tax_withholding_details.py | 89 +++++++++++++++---- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 0c324c8bf1..d0f243a327 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -63,16 +63,14 @@ def get_result( tax_amount += entry.credit - entry.debit # infer tax withholding category from the account if it's the single account for this category tax_withholding_category = tds_accounts.get(entry.account) - rate = tax_rate_map.get(tax_withholding_category) # or else the consolidated value from the voucher document if not tax_withholding_category: - # or else from the party default tax_withholding_category = tax_category_map.get(name) - rate = tax_rate_map.get(tax_withholding_category) + # or else from the party default if not tax_withholding_category: tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) + rate = tax_rate_map.get(tax_withholding_category) if net_total_map.get(name): if voucher_type == "Journal Entry": # back calcalute total amount from rate and tax_amount @@ -295,7 +293,7 @@ def get_tds_docs(filters): tds_accounts = {} for tds_acc in _tds_accounts: # if it turns out not to be the only tax withholding category, then don't include in the map - if tds_accounts.get(tds_acc["account"]): + if tds_acc["account"] in tds_accounts: tds_accounts[tds_acc["account"]] = None else: tds_accounts[tds_acc["account"]] = tds_acc["parent"] @@ -408,7 +406,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): "paid_amount_after_tax", "base_paid_amount", ], - "Journal Entry": ["tax_withholding_category", "total_amount"], + "Journal Entry": ["total_amount"], } entries = frappe.get_all( diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index b3f67378a9..7515616b0b 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -5,7 +5,6 @@ import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import today -from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -17,36 +16,63 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year -class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase): +class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.clear_old_entries() create_tax_accounts() - create_tcs_category() def test_tax_withholding_for_customers(self): + create_tax_category(cumulative_threshold=300) + frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS") si = create_sales_invoice(rate=1000) pe = create_tcs_payment_entry() + jv = create_tcs_journal_entry() + filters = frappe._dict( company="_Test Company", party_type="Customer", from_date=today(), to_date=today() ) result = execute(filters)[1] expected_values = [ + # Check for JV totals using back calculation logic + [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0], [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], [si.name, "TCS", 0.075, 1000, 0.52, 1000.52], ] self.check_expected_values(result, expected_values) + def test_single_account_for_multiple_categories(self): + create_tax_category("TDS - 1", rate=10, account="TDS - _TC") + inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_1.tax_withholding_category = "TDS - 1" + inv_1.submit() + + create_tax_category("TDS - 2", rate=20, account="TDS - _TC") + inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_2.tax_withholding_category = "TDS - 2" + inv_2.submit() + result = execute( + frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today()) + )[1] + expected_values = [ + [inv_1.name, "TDS - 1", 10, 5000, 500, 5500], + [inv_2.name, "TDS - 2", 20, 5000, 1000, 6000], + ] + self.check_expected_values(result, expected_values) + def check_expected_values(self, result, expected_values): for i in range(len(result)): voucher = frappe._dict(result[i]) voucher_expected_values = expected_values[i] - self.assertEqual(voucher.ref_no, voucher_expected_values[0]) - self.assertEqual(voucher.section_code, voucher_expected_values[1]) - self.assertEqual(voucher.rate, voucher_expected_values[2]) - self.assertEqual(voucher.base_total, voucher_expected_values[3]) - self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4]) - self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5]) + voucher_actual_values = ( + voucher.ref_no, + voucher.section_code, + voucher.rate, + voucher.base_total, + voucher.tax_amount, + voucher.grand_total, + ) + self.assertSequenceEqual(voucher_actual_values, voucher_expected_values) def tearDown(self): self.clear_old_entries() @@ -67,24 +93,20 @@ def create_tax_accounts(): ).insert(ignore_if_duplicate=True) -def create_tcs_category(): +def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0): fiscal_year = get_fiscal_year(today(), company="_Test Company") from_date = fiscal_year[1] to_date = fiscal_year[2] - tax_category = create_tax_withholding_category( - category_name="TCS", - rate=0.075, + create_tax_withholding_category( + category_name=category, + rate=rate, from_date=from_date, to_date=to_date, - account="TCS - _TC", - cumulative_threshold=300, + account=account, + cumulative_threshold=cumulative_threshold, ) - customer = frappe.get_doc("Customer", "_Test Customer") - customer.tax_withholding_category = "TCS" - customer.save() - def create_tcs_payment_entry(): payment_entry = create_payment_entry( @@ -109,3 +131,32 @@ def create_tcs_payment_entry(): ) payment_entry.submit() return payment_entry + + +def create_tcs_journal_entry(): + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = "_Test Company" + jv.set( + "accounts", + [ + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "credit_in_account_currency": 10000, + }, + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "debit_in_account_currency": 9992.5, + }, + { + "account": "TCS - _TC", + "debit_in_account_currency": 7.5, + }, + ], + ) + jv.insert() + return jv.submit() From ba0564874143a287c97773385eef9cb694a8dff6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:03:25 +0530 Subject: [PATCH 03/22] fix: add permissions to SRE (backport #39780) (#39786) fix: add permissions to SRE (cherry picked from commit 50f54d983d5bec92fab13fbd5226fe4cf2e62dd2) Co-authored-by: s-aga-r --- .../stock_reservation_entry.json | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json index 76cedd4b1e..bf5ea741e3 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json @@ -315,7 +315,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-19 16:41:16.545416", + "modified": "2024-02-07 16:05:17.772098", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reservation Entry", @@ -335,6 +335,90 @@ "share": 1, "submit": 1, "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 } ], "sort_field": "modified", From 5fd4ca56f5b38245a4066ff709faa157892496f2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 16:32:23 +0530 Subject: [PATCH 04/22] refactor: cancel Cr/Dr JE's on Sales/Purchase return cancel (cherry picked from commit 0549535603cae6a7afbd3375c8dd62b517af3c6e) --- erpnext/controllers/accounts_controller.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 81a7a101e5..fa4333ab47 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1472,6 +1472,24 @@ class AccountsController(TransactionBase): x.update({dim.fieldname: self.get(dim.fieldname)}) reconcile_against_document(lst, active_dimensions=active_dimensions) + def cancel_system_generated_credit_debit_notes(self): + # Cancel 'Credit/Debit' Note Journal Entries, if found. + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "reference_type": self.doctype, + "reference_name": self.name, + "voucher_type": voucher_type, + "docstatus": 1, + }, + pluck="name", + ) + for x in journals: + frappe.get_doc("Journal Entry", x).cancel() + def on_cancel(self): from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( remove_from_bank_transaction, @@ -1484,6 +1502,8 @@ class AccountsController(TransactionBase): remove_from_bank_transaction(self.doctype, self.name) if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + self.cancel_system_generated_credit_debit_notes() + # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) From 0bff065f0b707f0379f9f8d9db59319f87556ba1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:21:29 +0530 Subject: [PATCH 05/22] test: Invoice status on Cr/Dr note cancellation (cherry picked from commit 31a8c3bdc45f0e32a51e43260db8484e5f17aa75) --- .../test_payment_reconciliation.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index d7a73f0ce7..89240ac0d8 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -591,6 +591,66 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(si.status, "Paid") self.assertEqual(si.outstanding_amount, 0) + def test_invoice_status_after_cr_note_cancellation(self): + # This test case is made after the 'always standalone Credit/Debit notes' feature is introduced + transaction_date = nowdate() + amount = 100 + + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + cr_note = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note.is_return = 1 + cr_note.return_against = si.name + cr_note = cr_note.save().submit() + + pr = self.create_payment_reconciliation() + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + pr.get_unreconciled_entries() + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 1) + + # assert status outstanding + si.reload() + self.assertEqual(si.status, "Credit Note Issued") + self.assertEqual(si.outstanding_amount, 0) + + cr_note.reload() + cr_note.cancel() + # 'Credit Note' Journal should be auto cancelled + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 0) + def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() amount = 100 From a805796c0a912f4cadcaaa480bab8b4722c45834 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:33:55 +0530 Subject: [PATCH 06/22] refactor(test): assert Invoice status as well (cherry picked from commit 33efe0d12d85484e551dc9ebebf8840d427ecc67) --- .../payment_reconciliation/test_payment_reconciliation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 89240ac0d8..fb75a0f7ca 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -630,7 +630,7 @@ class TestPaymentReconciliation(FrappeTestCase): ) self.assertEqual(len(journals), 1) - # assert status outstanding + # assert status and outstanding si.reload() self.assertEqual(si.status, "Credit Note Issued") self.assertEqual(si.outstanding_amount, 0) @@ -650,6 +650,10 @@ class TestPaymentReconciliation(FrappeTestCase): pluck="name", ) self.assertEqual(len(journals), 0) + # assert status and outstanding + si.reload() + self.assertEqual(si.status, "Unpaid") + self.assertEqual(si.outstanding_amount, 100) def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() From 83bf2c17e01376da3a74624288ff46a9cf9cefa9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 19:59:33 +0530 Subject: [PATCH 07/22] refactor(test): Forex Credit Note cancellation against Invoice (cherry picked from commit 2f676ced5c712823c5737f40230ec8b1994cd2dd) --- .../controllers/tests/test_accounts_controller.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 3d6ebc02e6..fbdf22d5ae 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1041,18 +1041,18 @@ class TestAccountsController(FrappeTestCase): cr_note.reload() cr_note.cancel() - # Exchange Gain/Loss Journal should've been created. + # with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller + # JE(Credit Note) will be cancelled once the parent is cancelled exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 0) self.assertEqual(len(exc_je_for_cr), 0) - # The Credit Note JE is still active and is referencing the sales invoice - # So, outstanding stays the same + # No references, full outstanding si.reload() - self.assertEqual(si.outstanding_amount, 1) - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) def test_40_cost_center_from_payment_entry(self): """ From 60e04ab6618196a79d53ffab9e4ea02917615076 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 09:13:38 +0530 Subject: [PATCH 08/22] fix: incorrect planned qty in PP (backport #39785) (#39793) fix: incorrect planned qty in PP (cherry picked from commit a8ebc94a366e8f15e9bbeab3da064cf5f22dd1b9) Co-authored-by: s-aga-r --- .../doctype/production_plan/production_plan.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d460108d7b..f2ba1672fe 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -312,9 +312,10 @@ class ProductionPlan(Document): so_item.parent, so_item.item_code, so_item.warehouse, - ( - (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor - ).as_("pending_qty"), + so_item.qty, + so_item.work_order_qty, + so_item.delivered_qty, + so_item.conversion_factor, so_item.description, so_item.name, so_item.bom_no, @@ -337,6 +338,11 @@ class ProductionPlan(Document): items = items_query.run(as_dict=True) + for item in items: + item.pending_qty = ( + flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor + ) + pi = frappe.qb.DocType("Packed Item") packed_items_query = ( From 30daccecc70839553d69845963f7d3f6c7c09957 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:41:58 +0530 Subject: [PATCH 09/22] fix: do not consider rejected warehouses in pick list (backport #39539) (backport #39804) (#39811) * fix: do not consider rejected warehouses in pick list (backport #39539) (#39804) * fix: do not consider rejected warehouses in pick list (#39539) * fix: do not picked rejected materials * test: test case for pick list without rejected materials (cherry picked from commit f6725e43425043eaba7dcdd3cf3768a857a39ee6) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py # erpnext/stock/doctype/pick_list/pick_list.json # erpnext/stock/doctype/pick_list/pick_list.py * chore: fix conflicts * chore: fix conflicts * chore: fix conflicts * chore: fixed test case --------- Co-authored-by: rohitwaghchaure (cherry picked from commit 2c8e4c1ab3bde6a9ab37a41d2801c447d76903ef) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py * chore: fix conflicts --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: rohitwaghchaure --- .../doctype/sales_order/test_sales_order.py | 78 +++++++++++++++++++ .../stock/doctype/pick_list/pick_list.json | 12 ++- erpnext/stock/doctype/pick_list/pick_list.py | 74 ++++++++++++++++-- .../stock/doctype/warehouse/warehouse.json | 10 ++- 4 files changed, 165 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a518597aa6..681c2bdd0a 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import ( WarehouseRequired, + create_pick_list, make_delivery_note, make_material_request, make_raw_material_request, @@ -1973,6 +1974,83 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) + def test_pick_list_without_rejected_materials(self): + serial_and_batch_item = make_item( + "_Test Serial and Batch Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TSBIFRM-.#####", + "serial_no_series": "SN-TSBIFRM-.#####", + }, + ).name + + serial_item = make_item( + "_Test Serial Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "serial_no_series": "SN-TSIFRM-.#####", + }, + ).name + + batch_item = make_item( + "_Test Batch Item for Rejected Materials", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TBIFRM-.#####", + }, + ).name + + normal_item = make_item("_Test Normal Item for Rejected Materials").name + + warehouse = "_Test Warehouse - _TC" + rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC" + + if not frappe.db.exists("Warehouse", rejected_warehouse): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": rejected_warehouse, + "company": "_Test Company", + "warehouse_group": "_Test Warehouse Group", + "is_rejected_warehouse": 1, + } + ).insert() + + se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse}) + + se.save() + se.submit() + + se = make_stock_entry( + item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True + ) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse}) + + se.save() + se.submit() + + so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True) + + for item in [serial_and_batch_item, serial_item, batch_item]: + so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse}) + + so.save() + so.submit() + + pick_list = create_pick_list(so.name) + + pick_list.save() + for row in pick_list.locations: + self.assertEqual(row.qty, 1.0) + self.assertFalse(row.warehouse == rejected_warehouse) + self.assertTrue(row.warehouse == warehouse) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index bd84aadef7..0c474342a9 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -16,6 +16,7 @@ "for_qty", "column_break_4", "parent_warehouse", + "consider_rejected_warehouses", "get_item_locations", "section_break_6", "scan_barcode", @@ -184,11 +185,18 @@ "report_hide": 1, "reqd": 1, "search_index": 1 + }, + { + "default": "0", + "description": "Enable it if users want to consider rejected materials to dispatch.", + "fieldname": "consider_rejected_warehouses", + "fieldtype": "Check", + "label": "Consider Rejected Warehouses" } ], "is_submittable": 1, "links": [], - "modified": "2024-02-01 16:17:44.877426", + "modified": "2024-02-02 16:17:44.877426", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -260,4 +268,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index e2edb20510..98ed569af1 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -369,6 +369,7 @@ class PickList(Document): self.item_count_map.get(item_code), self.company, picked_item_details=picked_items_details.get(item_code), + consider_rejected_warehouses=self.consider_rejected_warehouses, ), ) @@ -710,6 +711,7 @@ def get_available_item_locations( company, ignore_validation=False, picked_item_details=None, + consider_rejected_warehouses=False, ): locations = [] total_picked_qty = ( @@ -725,18 +727,34 @@ def get_available_item_locations( required_qty, company, total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_batch_no: locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) else: locations = get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) total_qty_available = sum(location.get("qty") for location in locations) @@ -775,6 +793,7 @@ def get_available_item_locations_for_serial_and_batched_item( required_qty, company, total_picked_qty=0, + consider_rejected_warehouses=False, ): # Get batch nos by FIFO locations = get_available_item_locations_for_batched_item( @@ -782,6 +801,7 @@ def get_available_item_locations_for_serial_and_batched_item( from_warehouses, required_qty, company, + consider_rejected_warehouses=consider_rejected_warehouses, ) if locations: @@ -811,7 +831,12 @@ def get_available_item_locations_for_serial_and_batched_item( def get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses) @@ -828,6 +853,10 @@ def get_available_item_locations_for_serialized_item( else: query = query.where(Coalesce(sn.warehouse, "") != "") + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(sn.warehouse.notin(rejected_warehouses)) + serial_nos = query.run(as_list=True) warehouse_serial_nos_map = frappe._dict() @@ -860,7 +889,12 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): locations = [] data = get_auto_batch_nos( @@ -875,7 +909,14 @@ def get_available_item_locations_for_batched_item( ) warehouse_wise_batches = frappe._dict() + rejected_warehouses = get_rejected_warehouses() + for d in data: + if ( + not consider_rejected_warehouses and rejected_warehouses and d.warehouse in rejected_warehouses + ): + continue + if d.warehouse not in warehouse_wise_batches: warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float)) @@ -898,7 +939,12 @@ def get_available_item_locations_for_batched_item( def get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): bin = frappe.qb.DocType("Bin") query = ( @@ -915,6 +961,10 @@ def get_available_item_locations_for_other_item( wh = frappe.qb.DocType("Warehouse") query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company)) + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(bin.warehouse.notin(rejected_warehouses)) + item_locations = query.run(as_dict=True) return item_locations @@ -1236,3 +1286,15 @@ def update_common_item_properties(item, location): item.serial_no = location.serial_no item.batch_no = location.batch_no item.material_request_item = location.material_request_item + + +def get_rejected_warehouses(): + if not hasattr(frappe.local, "rejected_warehouses"): + frappe.local.rejected_warehouses = [] + + if not frappe.local.rejected_warehouses: + frappe.local.rejected_warehouses = frappe.get_all( + "Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name" + ) + + return frappe.local.rejected_warehouses diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 43b2ad2a69..7b0cade3ca 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -13,6 +13,7 @@ "column_break_3", "is_group", "parent_warehouse", + "is_rejected_warehouse", "column_break_4", "account", "company", @@ -249,13 +250,20 @@ { "fieldname": "column_break_qajx", "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If yes, then this warehouse will be used to store rejected materials", + "fieldname": "is_rejected_warehouse", + "fieldtype": "Check", + "label": "Is Rejected Warehouse" } ], "icon": "fa fa-building", "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-05-29 13:10:43.333160", + "modified": "2024-01-24 16:27:28.299520", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", From b625b05ddcd5cff7b51bc6e578c47d8eb76e662a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:32:51 +0530 Subject: [PATCH 10/22] fix: warehouse issue in pick list (backport #39826) (#39827) fix: warehouse issue in pick list (#39826) (cherry picked from commit 159a123dc79cd0a8ed6e6caa1532bc026264058f) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/pick_list/pick_list.js | 3 +++ erpnext/stock/doctype/pick_list/pick_list.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index aa0e125496..3cc2956e96 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -77,6 +77,9 @@ frappe.ui.form.on('Pick List', { }, freeze: 1, freeze_message: __("Setting Item Locations..."), + callback(r) { + refresh_field("locations"); + } }); } }, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 98ed569af1..0e1f8d78b8 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -348,9 +348,9 @@ class PickList(Document): picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() - from_warehouses = None + from_warehouses = [self.parent_warehouse] if self.parent_warehouse else [] if self.parent_warehouse: - from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) + from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse)) # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") From 63b4d20bdfbfbc894a255187bd2c45ac95782165 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 10 Feb 2024 15:58:33 +0530 Subject: [PATCH 11/22] fix(ux): set rate as price list rate on uom change in MR (backport #39816) (#39818) * fix: add price list rate field in MR Item (cherry picked from commit 61a29eb5fbd0535a4d91f2fd4c4e7fbb0edf6ace) * fix: set rate as price list rate on uom change (cherry picked from commit 5cf0759b0c6c41740b19197aa4e13722a806af97) * chore: linter (cherry picked from commit 1745371cd67939bf2d8bac9e0aac6478332c17de) --------- Co-authored-by: s-aga-r --- .../material_request/material_request.js | 20 +++++++++++++++---- .../material_request_item.json | 12 ++++++++++- .../material_request_item.py | 1 + 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index e80218a017..77a3d6d97f 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -228,9 +228,17 @@ frappe.ui.form.on('Material Request', { const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty']; if(!r.exc) { - $.each(r.message, function(k, v) { - if(!d[k] || in_list(qty_fields, k)) d[k] = v; + $.each(r.message, function(key, value) { + if(!d[key] || qty_fields.includes(key)) { + d[key] = value; + } }); + + if (d.price_list_rate != r.message.price_list_rate) { + d.price_list_rate = r.message.price_list_rate; + + frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate); + } } } }); @@ -432,7 +440,6 @@ frappe.ui.form.on("Material Request Item", { item.amount = flt(item.qty) * flt(item.rate); frappe.model.set_value(doctype, name, "amount", item.amount); refresh_field("amount", item.name, item.parentfield); - frm.events.get_item_data(frm, item, false); }, item_code: function(frm, doctype, name) { @@ -452,7 +459,12 @@ frappe.ui.form.on("Material Request Item", { set_schedule_date(frm); } } - } + }, + + conversion_factor: function(frm, doctype, name) { + const item = locals[doctype][name]; + frm.events.get_item_data(frm, item, false); + }, }); erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController { diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 5dc07c99f6..c7239b53e5 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -35,6 +35,7 @@ "received_qty", "rate_and_amount_section_break", "rate", + "price_list_rate", "col_break3", "amount", "accounting_details_section", @@ -473,13 +474,22 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "price_list_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Price List Rate", + "options": "currency", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:59.599115", + "modified": "2024-02-08 16:30:56.137858", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py index 2bed596292..d23d041f5f 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.py +++ b/erpnext/stock/doctype/material_request_item/material_request_item.py @@ -41,6 +41,7 @@ class MaterialRequestItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + price_list_rate: DF.Currency production_plan: DF.Link | None project: DF.Link | None projected_qty: DF.Float From 43fce29a044b377becda8f55b9056f7d4ee2de15 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 11 Feb 2024 11:28:48 +0530 Subject: [PATCH 12/22] fix: stock entry for use serial batch fields (backport #39843) (#39844) fix: stock entry for use serial batch fields (#39843) (cherry picked from commit e5824fc3f1f862ab625eea19f27f61ab74e0708d) Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 8 +++- .../stock/doctype/stock_entry/stock_entry.py | 3 ++ .../doctype/stock_entry/stock_entry_utils.py | 11 +++-- .../doctype/stock_entry/test_stock_entry.py | 46 +++++++++++++++++++ 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index ba3cdc8e83..549380cbb9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -156,14 +156,18 @@ class StockController(AccountsController): if self.doctype == "Stock Reconciliation": qty = row.qty type_of_transaction = "Inward" + warehouse = row.warehouse else: - qty = row.stock_qty + qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty type_of_transaction = get_type_of_transaction(self, row) + warehouse = ( + row.warehouse if self.doctype != "Stock Entry" else row.s_warehouse or row.t_warehouse + ) sn_doc = SerialBatchCreation( { "item_code": row.item_code, - "warehouse": row.warehouse, + "warehouse": warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, "voucher_type": self.doctype, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4239191383..dace69be0a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -977,6 +977,9 @@ class StockEntry(StockController): already_picked_serial_nos = [] for row in self.items: + if row.use_serial_batch_fields and (row.serial_no or row.batch_no): + continue + if not row.s_warehouse: continue diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 0f67e47ad9..271cbbc007 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -92,9 +92,6 @@ def make_stock_entry(**args): else: args.qty = cint(args.qty) - if args.serial_no or args.batch_no: - args.use_serial_batch_fields = True - # purpose if not args.purpose: if args.source and args.target: @@ -136,7 +133,7 @@ def make_stock_entry(**args): serial_number = args.serial_no bundle_id = None - if args.serial_no or args.batch_no or args.batches: + if not args.use_serial_batch_fields and (args.serial_no or args.batch_no or args.batches): batches = frappe._dict({}) if args.batch_no: batches = frappe._dict({args.batch_no: args.qty}) @@ -164,7 +161,11 @@ def make_stock_entry(**args): .name ) - args.serial_no = serial_number + args["serial_no"] = "" + args["batch_no"] = "" + + else: + args.serial_no = serial_number s.append( "items", diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 7ef2a0d5a0..571bef50f3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1587,6 +1587,7 @@ class TestStockEntry(FrappeTestCase): qty=4, to_warehouse="_Test Warehouse - _TC", batch_no=batch.name, + use_serial_batch_fields=1, do_not_save=True, ) @@ -1745,6 +1746,51 @@ class TestStockEntry(FrappeTestCase): mr.cancel() mr.delete() + def test_use_serial_and_batch_fields(self): + item = make_item( + "Test Use Serial and Batch Item SN Item", + {"has_serial_no": 1, "is_stock_item": 1}, + ) + + serial_nos = [ + "Test Use Serial and Batch Item SN Item - SN 001", + "Test Use Serial and Batch Item SN Item - SN 002", + ] + + se = make_stock_entry( + item_code=item.name, + qty=2, + to_warehouse="_Test Warehouse - _TC", + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + self.assertTrue(se.items[0].use_serial_batch_fields) + self.assertFalse(se.items[0].serial_no) + self.assertTrue(se.items[0].serial_and_batch_bundle) + + for serial_no in serial_nos: + self.assertTrue(frappe.db.exists("Serial No", serial_no)) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active") + + se1 = make_stock_entry( + item_code=item.name, + qty=2, + from_warehouse="_Test Warehouse - _TC", + use_serial_batch_fields=1, + serial_no="\n".join(serial_nos), + ) + + se1.reload() + + self.assertTrue(se1.items[0].use_serial_batch_fields) + self.assertFalse(se1.items[0].serial_no) + self.assertTrue(se1.items[0].serial_and_batch_bundle) + + for serial_no in serial_nos: + self.assertTrue(frappe.db.exists("Serial No", serial_no)) + self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + def make_serialized_item(**args): args = frappe._dict(args) From 2d5f1868129812ea1ce2cc8fda012ee66d4e7106 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 11 Feb 2024 17:29:21 +0530 Subject: [PATCH 13/22] feat: get RM costs from consumption entry in manufacture SE (backport #39822) (#39847) feat: get RM costs from consumption entry in manufacture SE (#39822) (cherry picked from commit 39067c761427e49c3c5aff5895faaee20de3f672) Co-authored-by: s-aga-r --- .../manufacturing_settings.json | 10 +++- .../manufacturing_settings.py | 1 + .../doctype/work_order/test_work_order.py | 46 ++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 60 +++++++++++++++++-- 4 files changed, 110 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index d3ad51f723..63e3fa3e9f 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -7,6 +7,7 @@ "field_order": [ "raw_materials_consumption_section", "material_consumption", + "get_rm_cost_from_consumption_entry", "column_break_3", "backflush_raw_materials_based_on", "capacity_planning", @@ -202,13 +203,20 @@ "fieldname": "set_op_cost_and_scrape_from_sub_assemblies", "fieldtype": "Check", "label": "Set Operating Cost / Scrape Items From Sub-assemblies" + }, + { + "default": "0", + "depends_on": "eval: doc.material_consumption", + "fieldname": "get_rm_cost_from_consumption_entry", + "fieldtype": "Check", + "label": "Get Raw Materials Cost from Consumption Entry" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-12-28 16:37:44.874096", + "modified": "2024-02-08 19:00:37.561244", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index 463ba9fe4b..9a501115b0 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -26,6 +26,7 @@ class ManufacturingSettings(Document): default_scrap_warehouse: DF.Link | None default_wip_warehouse: DF.Link | None disable_capacity_planning: DF.Check + get_rm_cost_from_consumption_entry: DF.Check job_card_excess_transfer: DF.Check make_serial_no_batch_from_work_order: DF.Check material_consumption: DF.Check diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index c8f3872622..010410732c 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1775,6 +1775,52 @@ class TestWorkOrder(FrappeTestCase): "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0 ) + @change_settings( + "Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1} + ) + def test_get_rm_cost_from_consumption_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + rm = make_item(properties={"is_stock_item": 1}).name + fg = make_item(properties={"is_stock_item": 1}).name + + make_stock_entry_test_record( + purpose="Material Receipt", + item_code=rm, + target="Stores - _TC", + qty=10, + basic_rate=100, + ) + make_stock_entry_test_record( + purpose="Material Receipt", + item_code=rm, + target="Stores - _TC", + qty=10, + basic_rate=200, + ) + + bom = make_bom(item=fg, raw_materials=[rm], rate=150).name + wo = make_wo_order_test_record( + production_item=fg, + bom_no=bom, + qty=10, + ) + + mte = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) + mte.items[0].s_warehouse = "Stores - _TC" + mte.insert().submit() + + mce = frappe.get_doc(make_stock_entry(wo.name, "Material Consumption for Manufacture", 10)) + mce.insert().submit() + + me = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10)) + me.insert().submit() + + valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10 + self.assertEqual(me.items[0].valuation_rate, valuation_rate) + def prepare_boms_for_sub_assembly_test(): if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index dace69be0a..0a96c6b41d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -899,14 +899,62 @@ class StockEntry(StockController): return flt(outgoing_items_cost / total_fg_qty) def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float: + settings = frappe.get_single("Manufacturing Settings") scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) - # Get raw materials cost from BOM if multiple material consumption entries - if not outgoing_items_cost and frappe.db.get_single_value( - "Manufacturing Settings", "material_consumption", cache=True - ): - bom_items = self.get_bom_raw_materials(finished_item_qty) - outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()]) + if settings.material_consumption: + if settings.get_rm_cost_from_consumption_entry and self.work_order: + + # Validate only if Material Consumption Entry exists for the Work Order. + if frappe.db.exists( + "Stock Entry", + { + "docstatus": 1, + "work_order": self.work_order, + "purpose": "Material Consumption for Manufacture", + }, + ): + for item in self.items: + if not item.is_finished_item and not item.is_scrap_item: + label = frappe.get_meta(settings.doctype).get_label("get_rm_cost_from_consumption_entry") + frappe.throw( + _( + "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials." + ).format( + item.idx, + frappe.bold(label), + frappe.bold("Manufacture"), + frappe.bold("Material Consumption for Manufacture"), + ) + ) + + if frappe.db.exists( + "Stock Entry", {"docstatus": 1, "work_order": self.work_order, "purpose": "Manufacture"} + ): + frappe.throw( + _("Only one {0} entry can be created against the Work Order {1}").format( + frappe.bold("Manufacture"), frappe.bold(self.work_order) + ) + ) + + SE = frappe.qb.DocType("Stock Entry") + SE_ITEM = frappe.qb.DocType("Stock Entry Detail") + + outgoing_items_cost = ( + frappe.qb.from_(SE) + .left_join(SE_ITEM) + .on(SE.name == SE_ITEM.parent) + .select(Sum(SE_ITEM.valuation_rate * SE_ITEM.transfer_qty)) + .where( + (SE.docstatus == 1) + & (SE.work_order == self.work_order) + & (SE.purpose == "Material Consumption for Manufacture") + ) + ).run()[0][0] or 0 + + elif not outgoing_items_cost: + bom_items = self.get_bom_raw_materials(finished_item_qty) + outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()]) return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty) From 4daee6d9c70e69a54222fda4f5b2f7052ca68b0b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 11 Feb 2024 18:12:18 +0530 Subject: [PATCH 14/22] fix: Brazilian COA for demo data creation (#39839) fix: Brazilian COA for demo data creation (#39839) fix: Brazilian COA (cherry picked from commit 4b1c851da17c2b9dcabb60194fbad6974fc60716) Co-authored-by: Deepesh Garg --- .../verified/br_planilha_de_contas.json | 126 +++++++++--------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json index a1dbddc243..45be1e3fe6 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/br_planilha_de_contas.json @@ -56,7 +56,9 @@ "Constru\u00e7\u00f5es em Andamento de Im\u00f3veis Destinados \u00e0 Venda": {}, "Estoques Destinados \u00e0 Doa\u00e7\u00e3o": {}, "Im\u00f3veis Destinados \u00e0 Venda": {}, - "Insumos (materiais diretos)": {}, + "Insumos (materiais diretos)": { + "account_type": "Stock" + }, "Insumos Agropecu\u00e1rios": {}, "Mercadorias para Revenda": {}, "Outras 11": {}, @@ -146,6 +148,65 @@ "root_type": "Asset" }, "CUSTOS DE PRODU\u00c7\u00c3O": { + "CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": { + "CUSTO DOS PRODUTOS VENDIDOS": { + "CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": { + "Custos dos Produtos Vendidos em Geral": { + "account_type": "Cost of Goods Sold" + }, + "Outros Custos 4": {}, + "account_type": "Cost of Goods Sold" + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": { + "Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {}, + "Custos dos Produtos para Assist\u00eancia Social - Vendidos": {}, + "Outras": {} + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": { + "Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {}, + "Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {}, + "Outros Custos 6": {} + }, + "CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": { + "Custos dos Produtos para Sa\u00fade - Gratuidades": {}, + "Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {}, + "Outros Custos 5": {} + }, + "account_type": "Cost of Goods Sold" + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS": { + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": { + "Custo dos Servi\u00e7os Prestados em Geral": {}, + "Outros Custos": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": { + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade 1": {}, + "Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {}, + "Outros Custos 2": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": { + "Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {}, + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade": {}, + "Custo dos Servi\u00e7os Prestados ao PROUNI": {}, + "Outros Custos 1": {} + }, + "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": { + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {}, + "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {}, + "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {}, + "Custo dos Servi\u00e7os Prestados a Gratuidade 2": {}, + "Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {}, + "Outros Custos 3": {} + } + } + }, "CUSTO DOS BENS E SERVI\u00c7OS PRODUZIDOS": { "CUSTO DOS PRODUTOS DE FABRICA\u00c7\u00c3O PR\u00d3PRIA PRODUZIDOS": { "Alimenta\u00e7\u00e3o do Trabalhador": {}, @@ -621,7 +682,9 @@ "Receita das Unidades Imobili\u00e1rias Vendidas": {}, "Receita de Exporta\u00e7\u00e3o Direta de Mercadorias e Produtos": {}, "Receita de Exporta\u00e7\u00e3o de Servi\u00e7os": {}, - "Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": {}, + "Receita de Loca\u00e7\u00e3o de Bens M\u00f3veis e Im\u00f3veis": { + "account_type": "Income Account" + }, "Receita de Vendas de Mercadorias e Produtos a Comercial Exportadora com Fim Espec\u00edfico de Exporta\u00e7\u00e3o": {} } } @@ -645,65 +708,6 @@ } }, "RESULTADO OPERACIONAL": { - "CUSTO DOS PRODUTOS E SERVI\u00c7OS VENDIDOS": { - "CUSTO DOS PRODUTOS VENDIDOS": { - "CUSTO DOS PRODUTOS VENDIDOS PARA AS DEMAIS ATIVIDADES": { - "Custos dos Produtos Vendidos em Geral": { - "account_type": "Cost of Goods Sold" - }, - "Outros Custos 4": {}, - "account_type": "Cost of Goods Sold" - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA ASSIST\u00caNCIA SOCIAL": { - "Custos dos Produtos para Assist\u00eancia Social - Gratuidades": {}, - "Custos dos Produtos para Assist\u00eancia Social - Vendidos": {}, - "Outras": {} - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA EDUCA\u00c7\u00c3O": { - "Custos dos Produtos para Educa\u00e7\u00e3o - Gratuidades": {}, - "Custos dos Produtos para Educa\u00e7\u00e3o - Vendidos": {}, - "Outros Custos 6": {} - }, - "CUSTO DOS PRODUTOS VENDIDOS PARA SA\u00daDE": { - "Custos dos Produtos para Sa\u00fade - Gratuidades": {}, - "Custos dos Produtos para Sa\u00fade \u2013 Vendidos": {}, - "Outros Custos 5": {} - }, - "account_type": "Cost of Goods Sold" - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS": { - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA AS DEMAIS ATIVIDADES": { - "Custo dos Servi\u00e7os Prestados em Geral": {}, - "Outros Custos": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA ASSIST\u00caNCIA SOCIAL": { - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 1": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 1": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade 1": {}, - "Custo dos Servi\u00e7os Prestados a Pacientes Particulares": {}, - "Outros Custos 2": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA EDUCA\u00c7\u00c3O": { - "Custo dos Servi\u00e7os Prestados a Alunos N\u00e3o Bolsistas": {}, - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias (Exceto PROUNI)": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade": {}, - "Custo dos Servi\u00e7os Prestados ao PROUNI": {}, - "Outros Custos 1": {} - }, - "CUSTO DOS SERVI\u00c7OS PRESTADOS PARA SA\u00daDE": { - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios SUS": {}, - "Custo dos Servi\u00e7os Prestados a Conv\u00eanios/Contratos/Parcerias 1": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es 2": {}, - "Custo dos Servi\u00e7os Prestados a Doa\u00e7\u00f5es/Subven\u00e7\u00f5es Vinculadas 2": {}, - "Custo dos Servi\u00e7os Prestados a Gratuidade 2": {}, - "Custo dos Servi\u00e7os Prestados a Pacientes Particulares 1": {}, - "Outros Custos 3": {} - } - } - }, "DESPESAS OPERACIONAIS": { "DESPESAS OPERACIONAIS 1": { "DESPESAS OPERACIONAIS 2": { From c643e70e2f79fdf11e1d033fb7efd71408450288 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:53:50 +0530 Subject: [PATCH 15/22] perf: cached get_last_purchase_details to fix performance issue (backport #39854) (#39856) perf: cached get_last_purchase_details to fix performance issue (#39854) (cherry picked from commit b966c06a4f7ec1d64e475a626ee934695c77a2a4) Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 3 +++ erpnext/stock/doctype/item/item.py | 1 + 2 files changed, 4 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 549380cbb9..2c43e189c8 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -942,6 +942,9 @@ class StockController(AccountsController): "Stock Reconciliation", ) + if not frappe.get_all("Putaway Rule", limit=1): + return + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: valid_doctype = False diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 2d9e11ab84..9341853633 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1122,6 +1122,7 @@ def validate_cancelled_item(item_code, docstatus=None): frappe.throw(_("Item {0} is cancelled").format(item_code)) +@frappe.request_cache def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details From 10f17dfcc8b5a2d38b064af2698c2e96f0309fb4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:17:36 +0530 Subject: [PATCH 16/22] perf: production plan submission (backport #39846) (#39860) perf: production plan submission (cherry picked from commit aa1c69dd7aa7731b699853bafcfc40d19d5ab70a) Co-authored-by: s-aga-r --- .../material_request_plan_item.json | 11 +++++++---- .../doctype/production_plan/production_plan.json | 5 +++-- .../doctype/production_plan/production_plan.py | 8 ++++---- .../manufacturing/doctype/work_order/work_order.json | 5 +++-- .../doctype/work_order_item/work_order_item.json | 5 +++-- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index d07bf0fa66..06c1b49755 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -38,7 +38,8 @@ "in_list_view": 1, "label": "Item Code", "options": "Item", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "item_name", @@ -53,7 +54,8 @@ "in_standard_filter": 1, "label": "For Warehouse", "options": "Warehouse", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "columns": 1, @@ -141,7 +143,8 @@ "fieldname": "from_warehouse", "fieldtype": "Link", "label": "From Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "search_index": 1 }, { "fetch_from": "item_code.safety_stock", @@ -199,7 +202,7 @@ ], "istable": 1, "links": [], - "modified": "2023-09-12 12:09:08.358326", + "modified": "2024-02-11 16:21:11.977018", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 257b60c486..54c3893928 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -298,7 +298,8 @@ "no_copy": 1, "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -436,7 +437,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-26 16:31:13.740777", + "modified": "2024-02-11 15:42:47.642481", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f2ba1672fe..990c081bb7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1768,23 +1768,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production +@frappe.request_cache def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") - query = ( + return ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) .select(table.name) + .distinct() .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) & (child.planned_qty > child.ordered_qty) ) - ).run(as_dict=True) - - return list(set([d.name for d in query])) + ).run(pluck="name") def get_raw_materials_of_sub_assembly_items( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 1996e19c37..63c74b61c4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -447,7 +447,8 @@ "no_copy": 1, "options": "Production Plan", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "production_plan_item", @@ -592,7 +593,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-11 18:35:49.852069", + "modified": "2024-02-11 15:47:13.454422", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index f354d45381..0f4d693544 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -36,7 +36,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item" + "options": "Item", + "search_index": 1 }, { "fieldname": "source_warehouse", @@ -141,7 +142,7 @@ ], "istable": 1, "links": [], - "modified": "2022-09-28 10:50:43.512562", + "modified": "2024-02-11 15:45:32.318374", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", From 92e6017a298bfb01679b5388f478a3055143a80e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:59:07 +0530 Subject: [PATCH 17/22] fix: create SBB for `transfer_qty` in SE (backport #39835) (#39863) fix: create SBB for `transfer_qty` in SE (cherry picked from commit d59caf08e6459c4f378c7bd6b032b84a899f8e20) Co-authored-by: s-aga-r --- erpnext/stock/doctype/stock_entry/stock_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0a96c6b41d..81be3d1fc1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1035,7 +1035,7 @@ class StockEntry(StockController): continue bundle_doc = None - if row.serial_and_batch_bundle and abs(row.qty) != abs( + if row.serial_and_batch_bundle and abs(row.transfer_qty) != abs( frappe.get_cached_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty") ): bundle_doc = SerialBatchCreation( @@ -1045,7 +1045,7 @@ class StockEntry(StockController): "serial_and_batch_bundle": row.serial_and_batch_bundle, "type_of_transaction": "Outward", "ignore_serial_nos": already_picked_serial_nos, - "qty": row.qty * -1, + "qty": row.transfer_qty * -1, } ).update_serial_and_batch_entries() elif not row.serial_and_batch_bundle: @@ -1057,7 +1057,7 @@ class StockEntry(StockController): "posting_time": self.posting_time, "voucher_type": self.doctype, "voucher_detail_no": row.name, - "qty": row.qty * -1, + "qty": row.transfer_qty * -1, "ignore_serial_nos": already_picked_serial_nos, "type_of_transaction": "Outward", "company": self.company, From a2f1a964f1d05f541bfd48dd24be419c3d5477db Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:09:03 +0530 Subject: [PATCH 18/22] fix: validate duplicate SBB (backport #39862) (#39866) * fix: validate duplicate SBB (cherry picked from commit 094ecc1f62a97d6580a72c9a9fdf176a6d7dc959) * test: duplicate SBB (cherry picked from commit 55e66db315518bda82e4eb77d8fc74f5b2c7d14d) --------- Co-authored-by: s-aga-r --- erpnext/controllers/stock_controller.py | 29 +++++++++++++++++++ .../test_serial_and_batch_bundle.py | 20 ++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2c43e189c8..f920706ba6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -46,6 +46,9 @@ class BatchExpiredError(frappe.ValidationError): class StockController(AccountsController): def validate(self): super(StockController, self).validate() + + if self.docstatus == 0: + self.validate_duplicate_serial_and_batch_bundle() if not self.get("is_return"): self.validate_inspection() self.validate_serialized_batch() @@ -55,6 +58,32 @@ class StockController(AccountsController): self.validate_internal_transfer() self.validate_putaway_capacity() + def validate_duplicate_serial_and_batch_bundle(self): + if sbb_list := [ + item.get("serial_and_batch_bundle") + for item in self.items + if item.get("serial_and_batch_bundle") + ]: + SLE = frappe.qb.DocType("Stock Ledger Entry") + data = ( + frappe.qb.from_(SLE) + .select(SLE.voucher_type, SLE.voucher_no, SLE.serial_and_batch_bundle) + .where( + (SLE.docstatus == 1) + & (SLE.serial_and_batch_bundle.notnull()) + & (SLE.serial_and_batch_bundle.isin(sbb_list)) + ) + .limit(1) + ).run(as_dict=True) + + if data: + data = data[0] + frappe.throw( + _("Serial and Batch Bundle {0} is already used in {1} {2}.").format( + frappe.bold(data.serial_and_batch_bundle), data.voucher_type, data.voucher_no + ) + ) + def make_gl_entries(self, gl_entries=None, from_repost=False): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index f430943708..88b262a8c6 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -4,7 +4,7 @@ import json import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today from erpnext.stock.doctype.item.test_item import make_item @@ -521,6 +521,24 @@ class TestSerialandBatchBundle(FrappeTestCase): make_serial_nos(item_code, serial_nos) self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) + @change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}) + def test_duplicate_serial_and_batch_bundle(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name + + serial_no = f"{item_code}-001" + serial_nos = [{"serial_no": serial_no, "qty": 1}] + make_serial_nos(item_code, serial_nos) + + pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no]) + pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True) + + pr1.reload() + pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle + + self.assertRaises(frappe.exceptions.ValidationError, pr2.save) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos From 5a66c8585c1fab062fe2f92068a4e59b1f2d2bc9 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:05:03 +0100 Subject: [PATCH 19/22] fix: calculate `stock_value_diff` `d.item_tax_amount` is already in base currency. (cherry picked from commit 5df585179812c9f6f4b8f9593c24ef29529c8258) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 28d55f6ce3..67e6ff90e7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -684,9 +684,7 @@ class PurchaseReceipt(BuyingController): ) stock_value_diff = ( - flt(d.base_net_amount) - + flt(d.item_tax_amount / self.conversion_rate) - + flt(d.landed_cost_voucher_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount) ) elif warehouse_account.get(d.warehouse): stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) From f08b424972b08e98f24cc0dc8d70a4cade3690cb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 13:21:55 +0530 Subject: [PATCH 20/22] fix: use correct field name in accounts controller (backport #39884) (#39897) fix: use correct field name in accounts controller (#39884) Changes to get advance payments in SI or PI from JV's (cherry picked from commit b124081065d475c5b042eaf898773b35f1ac5dc6) Co-authored-by: Rohit Gunjegaonkar <135806454+rohitg-pbspl@users.noreply.github.com> --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fa4333ab47..0f087d4854 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2651,7 +2651,7 @@ def get_advance_journal_entries( if order_list: q = q.where( - (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_type).isin(order_list)) + (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list)) ) q = q.orderby(journal_entry.posting_date) From a548f1294147675be76e448bde58c8bcb2fa5b9b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:57:40 +0530 Subject: [PATCH 21/22] fix: landed cost voucher not submitting because of incorrect reference (backport #39898) (#39900) fix: landed cost voucher not submitting because of incorrect reference (#39898) (cherry picked from commit 6239fd704b7d7a60c54b8042a8cc83b5c9e75eab) Co-authored-by: rohitwaghchaure --- erpnext/controllers/buying_controller.py | 4 ++-- .../landed_cost_voucher.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fb680100b7..27ac9d52f6 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -217,8 +217,8 @@ class BuyingController(SubcontractingController): lc_voucher_data = frappe.db.sql( """select sum(applicable_charges), cost_center from `tabLanded Cost Item` - where docstatus = 1 and purchase_receipt_item = %s""", - d.name, + where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""", + (d.name, self.name), ) d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index aa5b2793c5..30f02d60a5 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -65,6 +65,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() self.validate_receipt_documents() + self.validate_line_items() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -72,6 +73,26 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() + def validate_line_items(self): + for d in self.get("items"): + if ( + d.docstatus == 0 + and d.purchase_receipt_item + and not frappe.db.exists( + d.receipt_document_type + " Item", + {"name": d.purchase_receipt_item, "parent": d.receipt_document}, + ) + ): + frappe.throw( + _("Row {0}: {2} Item {1} does not exist in {2} {3}").format( + d.idx, + frappe.bold(d.purchase_receipt_item), + d.receipt_document_type, + frappe.bold(d.receipt_document), + ), + title=_("Incorrect Reference Document (Purchase Receipt Item)"), + ) + def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) From 88a7248888f377ccceb4b8e04064d56f25b3a58d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:58:28 +0530 Subject: [PATCH 22/22] fix: production plan issue with sales order (backport #39901) (#39904) fix: production plan issue with sales order (#39901) (cherry picked from commit d0df5df4a609a04d98ee97c9e56931672143587d) Co-authored-by: rohitwaghchaure --- .../manufacturing/doctype/production_plan/production_plan.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 990c081bb7..573585b171 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -652,7 +652,10 @@ class ProductionPlan(Document): "project": self.project, } - key = (d.item_code, d.sales_order, d.warehouse) + key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse) + if self.combine_items: + key = (d.item_code, d.sales_order, d.warehouse) + if not d.sales_order: key = (d.name, d.item_code, d.warehouse)