From e9f2ab5caf90237dad9a31db713d566712c4b84f Mon Sep 17 00:00:00 2001 From: Ashish Shah Date: Wed, 10 May 2023 10:51:20 +0530 Subject: [PATCH 01/19] refactor: use 'flt' for base_total_taxes_and_charges difference_amount calculation is broken, as calculation gives NaN. Fix is make frm.doc.base_total_taxes_and_charges as flt(frm.doc.base_total_taxes_and_charges) (cherry picked from commit ae4e56747c63f0335259cb0949ae1d429d2de2a0) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 969027dd97c..cc72c313238 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -905,7 +905,7 @@ frappe.ui.form.on('Payment Entry', { function(d) { return flt(d.amount) })); frm.set_value("difference_amount", difference_amount - total_deductions + - frm.doc.base_total_taxes_and_charges); + flt(frm.doc.base_total_taxes_and_charges)); frm.events.hide_unhide_fields(frm); }, From aa7fede0dc92509394b2f5d8643b604df315f033 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 16 May 2023 15:12:47 +0530 Subject: [PATCH 02/19] fix(ux): SCR consumed-qty read-only property (cherry picked from commit adf2474d9ded01ed89aedddd1fd3da3514cd68b0) --- .../subcontracting_receipt.js | 18 ++++-------------- .../subcontracting_receipt.py | 8 ++++++++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 45289b1dab5..4bf008ac406 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -76,26 +76,14 @@ frappe.ui.form.on('Subcontracting Receipt', { } }); - let batch_no_field = frm.get_docfield("items", "batch_no"); + let batch_no_field = frm.get_docfield('items', 'batch_no'); if (batch_no_field) { batch_no_field.get_route_options_for_new_doc = function(row) { return { - "item": row.doc.item_code + 'item': row.doc.item_code } }; } - - frappe.db.get_single_value('Buying Settings', 'backflush_raw_materials_of_subcontract_based_on').then(val => { - if (val == 'Material Transferred for Subcontract') { - frm.fields_dict['supplied_items'].grid.grid_rows.forEach((grid_row) => { - grid_row.docfields.forEach((df) => { - if (df.fieldname == 'consumed_qty') { - df.read_only = 0; - } - }); - }); - } - }); }, refresh: (frm) => { @@ -157,6 +145,8 @@ frappe.ui.form.on('Subcontracting Receipt', { } }); }, __('Get Items From')); + + frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); } }, diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 2c842622730..416f4f80a21 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -28,6 +28,14 @@ class SubcontractingReceipt(SubcontractingController): }, ] + def onload(self): + self.set_onload( + "backflush_based_on", + frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ), + ) + def update_status_updater_args(self): if cint(self.is_return): self.status_updater.extend( From 15c1af3d8a44ad25b1d681cfe30de75faf1c10bd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 17 May 2023 16:17:21 +0530 Subject: [PATCH 03/19] fix: consider 0 if rate/qty are null (backport #35338) (#35340) fix: consider 0 if rate/qty are null (#35338) [skip ci] (cherry picked from commit e5c86bc2e82202522bd803f66dc1bdc00f8e4432) Co-authored-by: Ankush Menat --- .../patches/v13_0/update_amt_in_work_order_required_items.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py index e37f291233e..64170edb718 100644 --- a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py +++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py @@ -7,4 +7,6 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "work_order") frappe.reload_doc("manufacturing", "doctype", "work_order_item") - frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""") + frappe.db.sql( + """UPDATE `tabWork Order Item` SET amount = ifnull(rate, 0.0) * ifnull(required_qty, 0.0)""" + ) From 48884366ea6ca177813f2101a6347e0c2237d9f3 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 11 May 2023 12:47:47 +0530 Subject: [PATCH 04/19] fix: Pick List Status (cherry picked from commit 9fb8b1827daa126087d3e237d135a96e73841e70) --- erpnext/stock/doctype/pick_list/pick_list.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 46d6e9e7578..74927c779cc 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -29,6 +29,7 @@ class PickList(Document): self.validate_for_qty() def before_save(self): + self.update_status() self.set_item_locations() # set percentage picked in SO @@ -89,20 +90,20 @@ class PickList(Document): self.update_reference_qty() self.update_sales_order_picking_status() - def update_status(self, status=None, update_modified=True): + def update_status(self, status=None): if not status: if self.docstatus == 0: status = "Draft" elif self.docstatus == 1: - if self.status == "Draft": - status = "Open" - elif target_document_exists(self.name, self.purpose): + if target_document_exists(self.name, self.purpose): status = "Completed" + else: + status = "Open" elif self.docstatus == 2: status = "Cancelled" if status: - frappe.db.set_value("Pick List", self.name, "status", status, update_modified=update_modified) + self.db_set("status", status) def update_reference_qty(self): packed_items = [] From 0a080efce26e99198a326009caa5d353a8fae862 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 17 May 2023 22:21:00 +0530 Subject: [PATCH 05/19] fix: depreciation schedule for existing assets [v14] (#35255) * fix: depreciation schedule for existing assets * chore: correct logic for existing assets and fix test --- erpnext/assets/doctype/asset/asset.py | 10 ++++++---- erpnext/assets/doctype/asset/test_asset.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index fe1fd98aa09..39e65a7b97b 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -425,7 +425,7 @@ class Asset(AccountsController): depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life skip_row = True - if depreciation_amount > 0: + if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0: self._add_depreciation_row( schedule_date, depreciation_amount, @@ -1287,9 +1287,11 @@ def get_straight_line_or_manual_depr_amount(asset, row): ) # if the Depreciation Schedule is being prepared for the first time else: - return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt( - row.total_number_of_depreciations - ) + return ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) def get_wdv_or_dd_depr_amount( diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 8a1df6f71ce..1968154383d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -649,7 +649,7 @@ class TestDepreciationMethods(AssetSetup): ) self.assertEqual(asset.status, "Draft") - expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]] + expected_schedules = [["2032-12-31", 42904.11, 90000.0]] schedules = [ [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules") From 2631224e497d173684c25eb683ac3c86539b2ac3 Mon Sep 17 00:00:00 2001 From: vishnu Date: Sun, 14 May 2023 09:35:38 +0000 Subject: [PATCH 06/19] fix: Creating landed cost voucher from connections (cherry picked from commit f2ceb003797332cb6c1fee7a5c3e67a2b2b8e3d1) --- erpnext/public/js/controllers/accounts.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index d943126018a..47b88a002bc 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -91,6 +91,12 @@ frappe.ui.form.on("Sales Invoice", { }); frappe.ui.form.on('Purchase Invoice', { + setup: (frm) => { + frm.make_methods = { + 'Landed Cost Voucher': function () { frm.trigger('create_landedcost_voucher') }, + } + }, + mode_of_payment: function(frm) { get_payment_mode_account(frm, frm.doc.mode_of_payment, function(account){ frm.set_value('cash_bank_account', account); @@ -99,6 +105,20 @@ frappe.ui.form.on('Purchase Invoice', { payment_terms_template: function() { cur_frm.trigger("disable_due_date"); + }, + + create_landedcost_voucher: function (frm) { + let lcv = frappe.model.get_new_doc('Landed Cost Voucher'); + lcv.company = frm.doc.company; + + let lcv_receipt = frappe.model.get_new_doc('Landed Cost Purchase Invoice'); + lcv_receipt.receipt_document_type = 'Purchase Invoice'; + lcv_receipt.receipt_document = frm.doc.name; + lcv_receipt.supplier = frm.doc.supplier; + lcv_receipt.grand_total = frm.doc.grand_total; + lcv.purchase_receipts = [lcv_receipt]; + + frappe.set_route("Form", lcv.doctype, lcv.name); } }); From bdf81a43c646703d05dc0a720fa0078331a03142 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 17 May 2023 13:40:33 +0530 Subject: [PATCH 07/19] fix: tds incorrectly calculated for invoice that are below threshold Two purchase invoices for the same supplier, using different tax withholding categories have this issue. | Category | single | cumulative | |----------+--------+------------| | cat1 | 100 | 500 | | cat2 | 1000 | 5000 | 1. PINV1 of net total: 105/- uses cat1. TDS is calculated as it breached single threshold 2. PINV2 of net total: 200/- uses cat2. TDS incorrectly calculated as PINV1 already has TDS calculated and 'consider_party_ledger_amount' is enabled. (cherry picked from commit 84b7c1bba09f89a390a17898b4d8fa665adcbc8f) --- .../tax_withholding_category/tax_withholding_category.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index ad3477ef3d3..1f2d9803739 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -302,7 +302,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): "docstatus": 1, } - if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice": + if doctype != "Sales Invoice": filters.update( {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} ) From 879946e2c80de336fd81a063c5699cce48d7ca10 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 May 2023 12:00:59 +0530 Subject: [PATCH 08/19] fix(test): cumulative threshold checks (cherry picked from commit 132846bbd107a921dfbfde9240f368bea9382cf7) --- .../test_tax_withholding_category.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 1e86cf5d2ef..bc4f6709fca 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -110,9 +110,9 @@ class TestTaxWithholdingCategory(unittest.TestCase): invoices.append(pi1) # Cumulative threshold is 30000 - # Threshold calculation should be on both the invoices - # TDS should be applied only on 1000 - self.assertEqual(pi1.taxes[0].tax_amount, 1000) + # Threshold calculation should be only on the Second invoice + # Second didn't breach, no TDS should be applied + self.assertEqual(pi1.taxes, []) for d in reversed(invoices): d.cancel() From d23b93a46246499ac07b4ccadfe8330a9b86157a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 May 2023 12:47:38 +0530 Subject: [PATCH 09/19] fix: possible type error on quotation -> sales order creation (cherry picked from commit b2290c6f57cfe8a9ddade44f673039a4600313c9) --- erpnext/selling/doctype/quotation/quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 693fc92ce93..61969fe8a91 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -288,7 +288,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) # sales team - for d in customer.get("sales_team"): + for d in customer.get("sales_team") or []: target.append( "sales_team", { From 3a0cdf30ce0e841f5867a4022d99bda85a2afcc5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 19 May 2023 18:53:40 +0530 Subject: [PATCH 10/19] feat: provision to make reposting entries from Stock and Account Value Comparison Report (cherry picked from commit 7b818e9d34c87ad2fd80493454bd86674ddb54c5) --- .../stock_and_account_value_comparison.js | 37 ++++++++++++++++++- .../stock_and_account_value_comparison.py | 33 +++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js index 7a170beec37..50a78a82588 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js @@ -33,5 +33,40 @@ frappe.query_reports["Stock and Account Value Comparison"] = { "fieldtype": "Date", "default": frappe.datetime.get_today(), }, - ] + ], + + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + }); + }, + + onload(report) { + report.page.add_inner_button(__("Create Reposting Entries"), function() { + let message = `
+

+ Reposting Entries will change the value of + accounts Stock In Hand, and Stock Expenses + in the Trial Balance report and will also change + the Balance Value in the Stock Balance report. +

+

Are you sure you want to create Reposting Entries?

+
+ `; + + frappe.confirm(__(message), () => { + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map(i => frappe.query_report.data[i]); + + frappe.call({ + method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries", + args: { + rows: selected_rows, + company: frappe.query_report.get_filter_values().company + } + }); + + }); + }); + } }; diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 106e877c4cd..b1da3ec1bd1 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.utils import get_link_to_form, parse_json import erpnext from erpnext.accounts.utils import get_currency_precision, get_stock_accounts @@ -134,3 +135,35 @@ def get_columns(filters): "width": "120", }, ] + + +@frappe.whitelist() +def create_reposting_entries(rows, company): + if isinstance(rows, str): + rows = parse_json(rows) + + entries = [] + for row in rows: + row = frappe._dict(row) + + try: + doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Transaction", + "status": "Queued", + "voucher_type": row.voucher_type, + "voucher_no": row.voucher_no, + "posting_date": row.posting_date, + "company": company, + "allow_nagative_stock": 1, + } + ).submit() + + entries.append(get_link_to_form("Repost Item Valuation", doc.name)) + except frappe.DuplicateEntryError: + pass + + if entries: + entries = ", ".join(entries) + frappe.msgprint(_(f"Reposting entries created: {entries}")) From abfd975eb6ff755f46b4879d6f962e49bf237ca7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 20 May 2023 19:45:59 +0530 Subject: [PATCH 11/19] chore: drop outdated navigation video new stuff is in work, this one is actually counter-productive rn. (cherry picked from commit ba61865ee647891f91d38b6a6cd4c2350a05962e) --- erpnext/setup/module_onboarding/home/home.json | 7 ++----- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_supplier/create_a_supplier.json | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index 516f12229c5..1fd96796cb2 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -25,15 +25,12 @@ "documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2023-05-16 13:13:24.043792", + "modified": "2023-05-20 19:45:03.936741", "modified_by": "Administrator", "module": "Setup", "name": "Home", "owner": "Administrator", "steps": [ - { - "step": "Navigation Help" - }, { "step": "Create an Item" }, @@ -47,7 +44,7 @@ "step": "Create a Quotation" } ], - "subtitle": "Item, Customer, Supplier, Navigation Help and Quotation", + "subtitle": "Item, Customer, Supplier and Quotation", "success_message": "You're ready to start your journey with ERPNext", "title": "Let's begin your journey with ERPNext" } \ No newline at end of file diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json index e1a8f908663..5b0fd419c25 100644 --- a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2023-05-16 12:54:54.112364", + "modified": "2023-05-16 20:01:34.202622", "modified_by": "Administrator", "name": "Create a Customer", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json index ef493fe00d1..4ac26e2879c 100644 --- a/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/setup/onboarding_step/create_a_supplier/create_a_supplier.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2023-05-16 12:55:08.610113", + "modified": "2023-05-19 15:32:55.069257", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", From 633a1703dcbfb54bab897259c023862791cfbff4 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Sun, 21 May 2023 15:07:38 +0530 Subject: [PATCH 12/19] fix: don't recalculate rate for SCR rejected warehouse SLE (cherry picked from commit 57ee473fa40981ccb89acc91c608b6387a6b8278) --- erpnext/controllers/subcontracting_controller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index c3fa894e895..1e9c4dc8478 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -689,7 +689,6 @@ class SubcontractingController(StockController): "actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor), "serial_no": cstr(item.rejected_serial_no).strip(), "incoming_rate": 0.0, - "recalculate_rate": 1, }, ) ) From ce601afc4eb682d61ff8157561c877f6323d2df6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 May 2023 22:23:37 +0530 Subject: [PATCH 13/19] feat: provision to skip available sub assembly items in the production plan (cherry picked from commit 64751ec4d9bfd7a7f6d7e5b9b959035dc4dc3ba9) --- .../production_plan/production_plan.json | 32 ++++- .../production_plan/production_plan.py | 111 +++++++++++++++++- .../production_plan_sub_assembly_item.json | 30 ++++- 3 files changed, 161 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index fdaa4a2a1d4..232f1cb2c44 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -35,8 +35,12 @@ "section_break_25", "prod_plan_references", "section_break_24", - "get_sub_assembly_items", "combine_sub_items", + "section_break_ucc4", + "skip_available_sub_assembly_item", + "column_break_igxl", + "get_sub_assembly_items", + "section_break_g4ip", "sub_assembly_items", "download_materials_request_plan_section_section", "download_materials_required", @@ -351,12 +355,12 @@ { "fieldname": "section_break_24", "fieldtype": "Section Break", - "hide_border": 1 + "hide_border": 1, + "label": "Sub Assembly Items" }, { "fieldname": "sub_assembly_items", "fieldtype": "Table", - "label": "Sub Assembly Items", "no_copy": 1, "options": "Production Plan Sub Assembly Item" }, @@ -392,13 +396,33 @@ "fieldname": "download_materials_request_plan_section_section", "fieldtype": "Section Break", "label": "Download Materials Request Plan Section" + }, + { + "default": "0", + "description": "System consider the projected quantity to check available or will be available sub-assembly items ", + "fieldname": "skip_available_sub_assembly_item", + "fieldtype": "Check", + "label": "Skip Available Sub Assembly Items" + }, + { + "fieldname": "section_break_ucc4", + "fieldtype": "Column Break", + "hide_border": 1 + }, + { + "fieldname": "section_break_g4ip", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_igxl", + "fieldtype": "Column Break" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-03-31 10:30:48.118932", + "modified": "2023-05-22 23:36:31.770517", "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 f9e68b916f8..e40539acf34 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -718,7 +718,9 @@ class ProductionPlan(Document): frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) bom_data = [] - get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) + + warehouse = row.warehouse if self.skip_available_sub_assembly_item else None + get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) sub_assembly_items_store.extend(bom_data) @@ -894,7 +896,9 @@ def download_raw_materials(doc, warehouses=None): build_csv_response(item_list, doc.name) -def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): +def get_exploded_items( + item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None +): bei = frappe.qb.DocType("BOM Explosion Item") bom = frappe.qb.DocType("BOM") item = frappe.qb.DocType("Item") @@ -1271,6 +1275,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d include_safety_stock = doc.get("include_safety_stock") so_item_details = frappe._dict() + + sub_assembly_items = {} + if doc.get("skip_available_sub_assembly_item"): + for d in doc.get("sub_assembly_items"): + sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty")) + for data in po_items: if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): data["include_exploded_items"] = 1 @@ -1296,10 +1306,24 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: - if data.get("include_exploded_items") and include_subcontracted_items: + if ( + data.get("include_exploded_items") + and doc.get("sub_assembly_items") + and doc.get("skip_available_sub_assembly_item") + ): + item_details = get_raw_materials_of_sub_assembly_items( + item_details, + company, + bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) + + elif data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM item_details = get_exploded_items( - item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty + item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc ) else: item_details = get_subitems( @@ -1456,12 +1480,22 @@ def get_item_data(item_code): } -def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): +def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0): data = get_bom_children(parent=bom_no) for d in data: if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) + + if warehouse: + bin_dict = get_bin_details(d, company, for_warehouse=warehouse) + + if bin_dict and bin_dict[0].projected_qty > 0: + if bin_dict[0].projected_qty > stock_qty: + continue + else: + stock_qty = stock_qty - bin_dict[0].projected_qty + bom_data.append( frappe._dict( { @@ -1481,7 +1515,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): ) if d.value: - get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1) + get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1) def set_default_warehouses(row, default_warehouses): @@ -1519,3 +1553,68 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): ) return reserved_qty_for_production_plan - reserved_qty_for_production + + +def get_raw_materials_of_sub_assembly_items( + item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 +): + + bei = frappe.qb.DocType("BOM Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + items = ( + frappe.qb.from_(bei) + .join(bom) + .on(bom.name == bei.parent) + .join(item) + .on(item.name == bei.item_code) + .left_join(item_default) + .on((item_default.parent == item.name) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + (IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"), + item.item_name, + item.name.as_("item_code"), + bei.description, + bei.stock_uom, + bei.bom_no, + item.min_order_qty, + bei.source_warehouse, + item.default_material_request_type, + item.min_order_qty, + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + item.safety_stock, + ) + .where( + (bei.docstatus == 1) + & (bom.name == bom_no) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bei.item_code, bei.stock_uom) + ).run(as_dict=True) + + for item in items: + key = (item.item_code, item.bom_no) + if item.bom_no and key in sub_assembly_items: + planned_qty = flt(sub_assembly_items[key]) + get_raw_materials_of_sub_assembly_items( + item_details, + company, + item.bom_no, + include_non_stock_items, + sub_assembly_items, + planned_qty=planned_qty, + ) + else: + if not item.conversion_factor and item.purchase_uom: + item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom) + + item_details.setdefault(item.get("item_code"), item) + + return item_details diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index 4eb6bf6ecfc..fde0404c019 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -28,7 +28,11 @@ "uom", "stock_uom", "column_break_22", - "description" + "description", + "section_break_4rxf", + "actual_qty", + "column_break_xfhm", + "projected_qty" ], "fields": [ { @@ -183,12 +187,34 @@ "fieldtype": "Datetime", "in_list_view": 1, "label": "Schedule Date" + }, + { + "fieldname": "section_break_4rxf", + "fieldtype": "Section Break" + }, + { + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": "Actual Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_xfhm", + "fieldtype": "Column Break" + }, + { + "fieldname": "projected_qty", + "fieldtype": "Float", + "label": "Projected Qty", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-28 13:50:15.116082", + "modified": "2023-05-22 17:52:34.708879", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", From f6857b4698a8f4820785250f8bdc1c71bd21b0ee Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 23 May 2023 01:02:16 +0530 Subject: [PATCH 14/19] test: test case to skip available qty for sub-assembly items (cherry picked from commit 9dd566c1e80320165475649a5c34542902e00e4e) --- .../production_plan/test_production_plan.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 4648d896cee..75b43ec1c30 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -926,6 +926,50 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) + def test_skip_available_qty_for_sub_assembly_items(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + bom_tree = { + "Fininshed Goods1 For SUB Test": { + "SubAssembly1 For SUB Test": {"ChildPart1 For SUB Test": {}}, + "SubAssembly2 For SUB Test": {}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=10, + ignore_existing_ordered_qty=1, + do_not_submit=1, + skip_available_sub_assembly_item=1, + warehouse="_Test Warehouse - _TC", + ) + + make_stock_entry( + item_code="SubAssembly1 For SUB Test", + qty=5, + rate=100, + target="_Test Warehouse - _TC", + ) + + self.assertTrue(plan.skip_available_sub_assembly_item) + + plan.get_sub_assembly_items() + + for row in plan.sub_assembly_items: + if row.production_item == "SubAssembly1 For SUB Test": + self.assertEqual(row.qty, 5) + + mr_items = get_items_for_material_requests(plan.as_dict()) + for row in mr_items: + row = frappe._dict(row) + if row.item_code == "ChildPart1 For SUB Test": + self.assertEqual(row.quantity, 5) + + if row.item_code == "SubAssembly2 For SUB Test": + self.assertEqual(row.quantity, 10) + def create_production_plan(**args): """ @@ -945,6 +989,7 @@ def create_production_plan(**args): "include_subcontracted_items": args.include_subcontracted_items or 0, "ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0, "get_items_from": "Sales Order", + "skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0, } ) @@ -958,6 +1003,7 @@ def create_production_plan(**args): "planned_qty": args.planned_qty or 1, "planned_start_date": args.planned_start_date or now_datetime(), "stock_uom": args.stock_uom or "Nos", + "warehouse": args.warehouse, }, ) From fb7d3b7878640a2088140c05636c5e557d36ae34 Mon Sep 17 00:00:00 2001 From: vishnu Date: Mon, 22 May 2023 18:00:50 +0000 Subject: [PATCH 15/19] fix: error while saving job card (cherry picked from commit a209fb4b64cfc43f6bf266a6f96275226d2c2707) --- erpnext/manufacturing/doctype/job_card/job_card.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 85061113cea..bb84b6f2017 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -120,7 +120,8 @@ "fieldname": "for_quantity", "fieldtype": "Float", "in_list_view": 1, - "label": "Qty To Manufacture" + "label": "Qty To Manufacture", + "reqd": 1 }, { "fieldname": "wip_warehouse", @@ -439,7 +440,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2022-11-09 15:02:44.490731", + "modified": "2023-05-22 23:26:57.589331", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", From 668b092f6bfe13728d4774db21078c6896032026 Mon Sep 17 00:00:00 2001 From: vishnu Date: Tue, 23 May 2023 04:27:50 +0000 Subject: [PATCH 16/19] fix: use flt instead of mandatory field (cherry picked from commit 8c34cc0e00980d1968e4daee07a88fd85a0b4a52) --- erpnext/manufacturing/doctype/job_card/job_card.json | 5 ++--- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index bb84b6f2017..316e586b7a2 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -120,8 +120,7 @@ "fieldname": "for_quantity", "fieldtype": "Float", "in_list_view": 1, - "label": "Qty To Manufacture", - "reqd": 1 + "label": "Qty To Manufacture" }, { "fieldname": "wip_warehouse", @@ -440,7 +439,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2023-05-22 23:26:57.589331", + "modified": "2023-05-23 09:56:43.826602", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 4a4046e47aa..a7d0b29f83c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -730,7 +730,7 @@ class JobCard(Document): self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] if self.docstatus < 2: - if self.for_quantity <= self.transferred_qty: + if flt(self.for_quantity) <= flt(self.transferred_qty): self.status = "Material Transferred" if self.time_logs: From 6df9b53682d4f24051bcc31f6d1212de102f09e5 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 19 May 2023 12:57:47 +0530 Subject: [PATCH 17/19] fix: Pick List TypeError (cherry picked from commit a1119171141d64eac0a6bb1227c101c93645d8be) --- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 74927c779cc..a9a9a1d6641 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -460,7 +460,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty ) - while remaining_stock_qty > 0 and available_locations: + while flt(remaining_stock_qty) > 0 and available_locations: item_location = available_locations.pop(0) item_location = frappe._dict(item_location) From 9b9772eb14c86d144d3d3a97eb683f8679787d4a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 23 May 2023 15:36:56 +0530 Subject: [PATCH 18/19] fix: replace quotation with invoice in first onboarding (backport #35389) (#35394) fix: replace quotation with invoice in first onboarding (#35389) (cherry picked from commit b0eb72ffac0f35cd04c79098c1087648aefb52ba) Co-authored-by: Ankush Menat --- .../sales_invoice/sales_invoice.json | 41 +++++++++++++++++++ .../form_tour/quotation/quotation.json | 12 ++---- .../setup/module_onboarding/home/home.json | 7 +--- .../create_a_customer/create_a_customer.json | 2 +- .../create_an_item/create_an_item.json | 2 +- .../create_your_first_sales_invoice.json | 20 +++++++++ 6 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 erpnext/accounts/form_tour/sales_invoice/sales_invoice.json create mode 100644 erpnext/setup/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json diff --git a/erpnext/accounts/form_tour/sales_invoice/sales_invoice.json b/erpnext/accounts/form_tour/sales_invoice/sales_invoice.json new file mode 100644 index 00000000000..414b897d547 --- /dev/null +++ b/erpnext/accounts/form_tour/sales_invoice/sales_invoice.json @@ -0,0 +1,41 @@ +{ + "creation": "2023-05-23 09:58:17.235916", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "modified": "2023-05-23 13:10:56.227127", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Invoice", + "owner": "Administrator", + "reference_doctype": "Sales Invoice", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer for whom this invoice is being prepared.", + "fieldname": "customer", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Customer", + "next_step_condition": "eval: doc.customer", + "position": "Right", + "title": "Select Customer" + }, + { + "child_doctype": "Sales Invoice Item", + "description": "Select item that you have sold along with quantity and rate.", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "parent_fieldname": "items", + "position": "Top", + "title": "Select Item" + } + ], + "title": "Sales Invoice" +} \ No newline at end of file diff --git a/erpnext/selling/form_tour/quotation/quotation.json b/erpnext/selling/form_tour/quotation/quotation.json index 2a2aa5e63e4..8c977009fb6 100644 --- a/erpnext/selling/form_tour/quotation/quotation.json +++ b/erpnext/selling/form_tour/quotation/quotation.json @@ -2,9 +2,11 @@ "creation": "2021-11-23 12:00:36.138824", "docstatus": 0, "doctype": "Form Tour", + "first_document": 0, "idx": 0, + "include_name_field": 0, "is_standard": 1, - "modified": "2021-11-23 12:02:48.010298", + "modified": "2023-05-23 12:51:48.684517", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -14,51 +16,43 @@ "steps": [ { "description": "Select a customer or lead for whom this quotation is being prepared. Let's select a Customer.", - "field": "", "fieldname": "quotation_to", "fieldtype": "Link", "has_next_condition": 0, "is_table_field": 0, "label": "Quotation To", - "parent_field": "", "position": "Right", "title": "Quotation To" }, { "description": "Select a specific Customer to whom this quotation will be sent.", - "field": "", "fieldname": "party_name", "fieldtype": "Dynamic Link", "has_next_condition": 0, "is_table_field": 0, "label": "Party", - "parent_field": "", "position": "Right", "title": "Party" }, { "child_doctype": "Quotation Item", "description": "Select an item for which you will be quoting a price.", - "field": "", "fieldname": "items", "fieldtype": "Table", "has_next_condition": 0, "is_table_field": 0, "label": "Items", - "parent_field": "", "parent_fieldname": "items", "position": "Bottom", "title": "Items" }, { "description": "You can select pre-populated Sales Taxes and Charges from here.", - "field": "", "fieldname": "taxes", "fieldtype": "Table", "has_next_condition": 0, "is_table_field": 0, "label": "Sales Taxes and Charges", - "parent_field": "", "position": "Bottom", "title": "Sales Taxes and Charges" } diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index 1fd96796cb2..7b0d77ff503 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -25,7 +25,7 @@ "documentation_url": "https://docs.erpnext.com/docs/v14/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2023-05-20 19:45:03.936741", + "modified": "2023-05-23 13:20:19.703506", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -38,10 +38,7 @@ "step": "Create a Customer" }, { - "step": "Create a Supplier" - }, - { - "step": "Create a Quotation" + "step": "Create Your First Sales Invoice" } ], "subtitle": "Item, Customer, Supplier and Quotation", diff --git a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json index 5b0fd419c25..dc075781221 100644 --- a/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/setup/onboarding_step/create_a_customer/create_a_customer.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2023-05-16 20:01:34.202622", + "modified": "2023-05-23 12:45:55.138580", "modified_by": "Administrator", "name": "Create a Customer", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json index 15f36bec81d..4115196ffa1 100644 --- a/erpnext/setup/onboarding_step/create_an_item/create_an_item.json +++ b/erpnext/setup/onboarding_step/create_an_item/create_an_item.json @@ -11,7 +11,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2023-05-16 12:56:40.355878", + "modified": "2023-05-23 12:43:08.484206", "modified_by": "Administrator", "name": "Create an Item", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json b/erpnext/setup/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json new file mode 100644 index 00000000000..96fa68e8c90 --- /dev/null +++ b/erpnext/setup/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json @@ -0,0 +1,20 @@ +{ + "action": "Create Entry", + "creation": "2020-05-14 17:48:21.019019", + "description": "# All about sales invoice\n\nA Sales Invoice is a bill that you send to your Customers against which the Customer makes the payment. Sales Invoice is an accounting transaction. On submission of Sales Invoice, the system updates the receivable and books income against a Customer Account.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2023-05-22 21:20:15.589644", + "modified_by": "Administrator", + "name": "Create Your First Sales Invoice", + "owner": "Administrator", + "reference_document": "Sales Invoice", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create Your First Sales Invoice ", + "validate_action": 1 +} \ No newline at end of file From 8af6a113d1bb4993b5310a2365dd7a85454b14fe Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 23 May 2023 23:04:04 +0530 Subject: [PATCH 19/19] fix: incorrect depr schedule and posting dates on selling of existing assets [v14] (#35396) * fix: use date in asset get gl entries functions * fix: calc depr amount properly on selling of existing assets * fix: calc depr amount properly on selling of existing assets again * chore: remove unnecessary line breaks --- .../doctype/sales_invoice/sales_invoice.py | 14 +++- erpnext/assets/doctype/asset/asset.py | 11 ++- erpnext/assets/doctype/asset/depreciation.py | 32 +++++--- erpnext/assets/doctype/asset/test_asset.py | 73 +++++++++++++++++++ .../asset_capitalization.py | 1 + 5 files changed, 114 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bb85fedab05..0dcd2e291a6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1179,7 +1179,12 @@ class SalesInvoice(SellingController): if self.is_return: fixed_asset_gl_entries = get_gl_entries_on_asset_regain( - asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") + asset, + item.base_net_amount, + item.finance_book, + self.get("doctype"), + self.get("name"), + self.get("posting_date"), ) asset.db_set("disposal_date", None) @@ -1194,7 +1199,12 @@ class SalesInvoice(SellingController): asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( - asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") + asset, + item.base_net_amount, + item.finance_book, + self.get("doctype"), + self.get("name"), + self.get("posting_date"), ) asset.db_set("disposal_date", self.posting_date) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 39e65a7b97b..8754bb7125a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -343,7 +343,7 @@ class Asset(AccountsController): # if asset is being sold if date_of_disposal: - from_date = self.get_from_date(finance_book.finance_book) + from_date = self.get_from_date_for_disposal(finance_book) depreciation_amount, days, months = self.get_pro_rata_amt( finance_book, depreciation_amount, @@ -500,16 +500,19 @@ class Asset(AccountsController): return start - def get_from_date(self, finance_book): + def get_from_date_for_disposal(self, finance_book): if not self.get("schedules"): - return self.available_for_use_date + return add_months( + getdate(self.available_for_use_date), + (self.number_of_depreciations_booked * finance_book.frequency_of_depreciation), + ) if len(self.finance_books) == 1: return self.schedules[-1].schedule_date from_date = "" for schedule in self.get("schedules"): - if schedule.finance_book == finance_book: + if schedule.finance_book == finance_book.finance_book: from_date = schedule.schedule_date if from_date: diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a1d29f88cb1..163752b65eb 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -272,7 +272,7 @@ def scrap_asset(asset_name): je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) - for entry in get_gl_entries_on_asset_disposal(asset): + for entry in get_gl_entries_on_asset_disposal(asset, date): entry.update({"reference_type": "Asset", "reference_name": asset_name}) je.append("accounts", entry) @@ -395,8 +395,11 @@ def disposal_happens_in_the_future(posting_date_of_disposal): def get_gl_entries_on_asset_regain( - asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None ): + if not date: + date = getdate() + ( fixed_asset_account, asset, @@ -414,7 +417,7 @@ def get_gl_entries_on_asset_regain( "debit_in_account_currency": asset.gross_purchase_amount, "debit": asset.gross_purchase_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -424,7 +427,7 @@ def get_gl_entries_on_asset_regain( "credit_in_account_currency": accumulated_depr_amount, "credit": accumulated_depr_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -433,7 +436,7 @@ def get_gl_entries_on_asset_regain( profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) if profit_amount: get_profit_gl_entries( - asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date ) if voucher_type and voucher_no: @@ -445,8 +448,11 @@ def get_gl_entries_on_asset_regain( def get_gl_entries_on_asset_disposal( - asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None ): + if not date: + date = getdate() + ( fixed_asset_account, asset, @@ -464,7 +470,7 @@ def get_gl_entries_on_asset_disposal( "credit_in_account_currency": asset.gross_purchase_amount, "credit": asset.gross_purchase_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -474,7 +480,7 @@ def get_gl_entries_on_asset_disposal( "debit_in_account_currency": accumulated_depr_amount, "debit": accumulated_depr_amount, "cost_center": depreciation_cost_center, - "posting_date": getdate(), + "posting_date": date, }, item=asset, ), @@ -483,7 +489,7 @@ def get_gl_entries_on_asset_disposal( profit_amount = flt(selling_amount) - flt(value_after_depreciation) if profit_amount: get_profit_gl_entries( - asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date ) if voucher_type and voucher_no: @@ -517,8 +523,12 @@ def get_asset_details(asset, finance_book=None): def get_profit_gl_entries( - asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None ): + + if not date: + date = getdate() + debit_or_credit = "debit" if profit_amount < 0 else "credit" gl_entries.append( asset.get_gl_dict( @@ -527,7 +537,7 @@ def get_profit_gl_entries( "cost_center": depreciation_cost_center, debit_or_credit: abs(profit_amount), debit_or_credit + "_in_account_currency": abs(profit_amount), - "posting_date": getdate(), + "posting_date": date, }, item=asset, ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 1968154383d..1c0972f2374 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -327,6 +327,79 @@ class TestAsset(AssetSetup): si.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + def test_gle_made_by_asset_sale_for_existing_asset(self): + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2020-04-01", + purchase_date="2020-04-01", + expected_value_after_useful_life=0, + total_number_of_depreciations=5, + number_of_depreciations_booked=2, + frequency_of_depreciation=12, + depreciation_start_date="2023-03-31", + opening_accumulated_depreciation=24000, + gross_purchase_amount=60000, + submit=1, + ) + + expected_depr_values = [ + ["2023-03-31", 12000, 36000], + ["2024-03-31", 12000, 48000], + ["2025-03-31", 12000, 60000], + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_depr_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_depr_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_depr_values[i][2], schedule.accumulated_depreciation_amount) + + post_depreciation_entries(date="2023-03-31") + + si = create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23") + ) + asset.load_from_db() + + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) + self.assertEqual(expected_values[i][1], schedule.depreciation_amount) + self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) + self.assertTrue(schedule.journal_entry) + + expected_gle = ( + ( + "_Test Accumulated Depreciations - _TC", + 37742.47, + 0.0, + ), + ( + "_Test Fixed Asset - _TC", + 0.0, + 60000.0, + ), + ( + "_Test Gain/Loss on Asset Disposal - _TC", + 0.0, + 17742.47, + ), + ("Debtors - _TC", 40000.0, 0.0), + ) + + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no = %s + order by account""", + si.name, + ) + + self.assertSequenceEqual(gle, expected_gle) + def test_asset_with_maintenance_required_status_after_sale(self): asset = create_asset( calculate_depreciation=1, diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 90b4d7e5834..f648823b5bf 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -436,6 +436,7 @@ class AssetCapitalization(StockController): item.get("finance_book") or self.get("finance_book"), self.get("doctype"), self.get("name"), + self.get("posting_date"), ) asset.db_set("disposal_date", self.posting_date)