From ed79b224ccaf482d6c3562cca99dc484dad73fd2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 20:52:02 +0530 Subject: [PATCH 001/376] fix(Asset): Group buttons under 'Action' --- erpnext/assets/doctype/asset/asset.js | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 6f1bb28f370..657c169fd16 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -78,36 +78,36 @@ frappe.ui.form.on('Asset', { frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.events.make_schedules_editable(frm); + if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { + frm.add_custom_button("General Ledger", function() { + frappe.route_options = { + "voucher_no": frm.doc.name, + "from_date": frm.doc.available_for_use_date, + "to_date": frm.doc.available_for_use_date, + "company": frm.doc.company + }; + frappe.set_route("query-report", "General Ledger"); + }); + } + if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { frm.add_custom_button("Transfer Asset", function() { erpnext.asset.transfer_asset(frm); - }); + }, __("Actions")); frm.add_custom_button("Scrap Asset", function() { erpnext.asset.scrap_asset(frm); - }); + }, __("Actions")); frm.add_custom_button("Sell Asset", function() { frm.trigger("make_sales_invoice"); - }); + }, __("Actions")); } else if (frm.doc.status=='Scrapped') { frm.add_custom_button("Restore Asset", function() { erpnext.asset.restore_asset(frm); - }); - } - - if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("General Ledger", function() { - frappe.route_options = { - "voucher_no": frm.doc.name, - "from_date": frm.doc.available_for_use_date, - "to_date": frm.doc.available_for_use_date, - "company": frm.doc.company - }; - frappe.set_route("query-report", "General Ledger"); - }); + }, __("Actions")); } if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { From 322975a03c98bfa72d3372c9cadca0be892855fb Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 21:09:13 +0530 Subject: [PATCH 002/376] feat(Asset): Add 'Create > Asset Repair' button --- erpnext/assets/doctype/asset/asset.js | 24 ++++++++++++++++++++++++ erpnext/assets/doctype/asset/asset.py | 12 +++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 657c169fd16..1f3978c0b07 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -115,6 +115,15 @@ frappe.ui.form.on('Asset', { frm.trigger("create_asset_maintenance"); }, __('Create')); } + if (frm.doc.docstatus == 1) { + frm.add_custom_button(__("Asset Repair"), function() { + // frappe.model.open_mapped_doc({ + // method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.make_expense_claim', + // frm: cur_frm, + // }); + frm.trigger("create_asset_repair"); + }, __("Create")); + } if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Asset Value Adjustment"), function() { frm.trigger("create_asset_adjustment"); @@ -304,6 +313,21 @@ frappe.ui.form.on('Asset', { }) }, + create_asset_repair: function(frm) { + frappe.call({ + args: { + "asset": frm.doc.name, + "item_code": frm.doc.item_code, + "item_name": frm.doc.item_name + }, + method: "erpnext.assets.doctype.asset.asset.create_asset_repair", + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }) + }, + create_asset_adjustment: function(frm) { frappe.call({ args: { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9aff1440d6a..962f78fa662 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -637,9 +637,19 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan }) return asset_maintenance +@frappe.whitelist() +def create_asset_repair(asset, item_code, item_name): + asset_repair = frappe.new_doc("Asset Repair") + asset_repair.update({ + "asset_name": asset, + "item_code": item_code, + "item_name": item_name + }) + return asset_repair + @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.new_doc("Asset Value Adjustment") + asset_maintenance = frappe.get_doc("Asset Value Adjustment") asset_maintenance.update({ "asset": asset, "company": company, From 3e0e3f0e1b062e38312950bc26b2ca29c86ee47b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 23:08:12 +0530 Subject: [PATCH 003/376] fix(Asset Repair): Replace 'Item Name' and 'Item Code' with 'Asset Name' --- erpnext/assets/doctype/asset/asset.js | 3 +- erpnext/assets/doctype/asset/asset.py | 7 ++-- .../doctype/asset_repair/asset_repair.json | 42 ++++++++----------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 1f3978c0b07..92a6648c638 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -317,8 +317,7 @@ frappe.ui.form.on('Asset', { frappe.call({ args: { "asset": frm.doc.name, - "item_code": frm.doc.item_code, - "item_name": frm.doc.item_name + "asset_name": frm.doc.asset_name }, method: "erpnext.assets.doctype.asset.asset.create_asset_repair", callback: function(r) { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 962f78fa662..b9f413854f7 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -638,12 +638,11 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan return asset_maintenance @frappe.whitelist() -def create_asset_repair(asset, item_code, item_name): +def create_asset_repair(asset, asset_name): asset_repair = frappe.new_doc("Asset Repair") asset_repair.update({ - "asset_name": asset, - "item_code": item_code, - "item_name": item_name + "asset": asset, + "asset_name": asset_name }) return asset_repair diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index d338fc0fb79..853534eb319 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -8,10 +8,9 @@ "engine": "InnoDB", "field_order": [ "naming_series", - "asset_name", "column_break_2", - "item_code", - "item_name", + "asset", + "asset_name", "section_break_5", "failure_date", "assign_to", @@ -30,15 +29,6 @@ "amended_from" ], "fields": [ - { - "columns": 1, - "fieldname": "asset_name", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Asset", - "options": "Asset", - "reqd": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -50,18 +40,6 @@ "fieldname": "column_break_2", "fieldtype": "Column Break" }, - { - "fetch_from": "asset_name.item_code", - "fieldname": "item_code", - "fieldtype": "Read Only", - "label": "Item Code" - }, - { - "fetch_from": "asset_name.item_name", - "fieldname": "item_name", - "fieldtype": "Read Only", - "label": "Item Name" - }, { "fieldname": "section_break_5", "fieldtype": "Section Break", @@ -159,12 +137,26 @@ "options": "Asset Repair", "print_hide": 1, "read_only": 1 + }, + { + "columns": 1, + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fieldname": "asset_name", + "fieldtype": "Read Only", + "label": "Asset Name" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-01-22 15:08:12.495850", + "modified": "2021-05-10 22:48:42.165513", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From bbbd61dab8830a7be0689b7d7b26f4873ee452dc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 23:24:34 +0530 Subject: [PATCH 004/376] fix(Asset): Group all buttons under 'Manage' --- erpnext/assets/doctype/asset/asset.js | 60 +++++++++++++-------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 92a6648c638..2a57183a802 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -78,65 +78,61 @@ frappe.ui.form.on('Asset', { frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.events.make_schedules_editable(frm); - if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("General Ledger", function() { - frappe.route_options = { - "voucher_no": frm.doc.name, - "from_date": frm.doc.available_for_use_date, - "to_date": frm.doc.available_for_use_date, - "company": frm.doc.company - }; - frappe.set_route("query-report", "General Ledger"); - }); - } - if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { frm.add_custom_button("Transfer Asset", function() { erpnext.asset.transfer_asset(frm); - }, __("Actions")); + }, __("Manage")); frm.add_custom_button("Scrap Asset", function() { erpnext.asset.scrap_asset(frm); - }, __("Actions")); + }, __("Manage")); frm.add_custom_button("Sell Asset", function() { frm.trigger("make_sales_invoice"); - }, __("Actions")); + }, __("Manage")); } else if (frm.doc.status=='Scrapped') { frm.add_custom_button("Restore Asset", function() { erpnext.asset.restore_asset(frm); - }, __("Actions")); + }, __("Manage")); } if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { - frm.add_custom_button(__("Asset Maintenance"), function() { + frm.add_custom_button(__("Maintain Asset"), function() { frm.trigger("create_asset_maintenance"); - }, __('Create')); - } - if (frm.doc.docstatus == 1) { - frm.add_custom_button(__("Asset Repair"), function() { - // frappe.model.open_mapped_doc({ - // method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.make_expense_claim', - // frm: cur_frm, - // }); - frm.trigger("create_asset_repair"); - }, __("Create")); + }, __("Manage")); } + + frm.add_custom_button(__("Repair Asset"), function() { + frm.trigger("create_asset_repair"); + }, __("Manage")); + if (frm.doc.status != 'Fully Depreciated') { - frm.add_custom_button(__("Asset Value Adjustment"), function() { + frm.add_custom_button(__("Adjust Asset Value"), function() { frm.trigger("create_asset_adjustment"); - }, __('Create')); + }, __("Manage")); } if (!frm.doc.calculate_depreciation) { - frm.add_custom_button(__("Depreciation Entry"), function() { + frm.add_custom_button(__("Create Depreciation Entry"), function() { frm.trigger("make_journal_entry"); - }, __('Create')); + }, __("Manage")); } - frm.page.set_inner_btn_group_as_primary(__('Create')); + if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { + frm.add_custom_button("View General Ledger", function() { + frappe.route_options = { + "voucher_no": frm.doc.name, + "from_date": frm.doc.available_for_use_date, + "to_date": frm.doc.available_for_use_date, + "company": frm.doc.company + }; + frappe.set_route("query-report", "General Ledger"); + }, __("Manage")); + } + + frm.page.set_inner_btn_group_as_primary(__("Manage")); frm.trigger("setup_chart"); } From 1c26b27a8929667961d1a587db14bc74f480eb3f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 00:08:39 +0530 Subject: [PATCH 005/376] fix(Asset Repair): Display 'Completion Date' and 'Repair Status' only after save --- erpnext/assets/doctype/asset_repair/asset_repair.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 4ba2b4474a9..f5eeeda5fb1 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,6 +2,10 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { + refresh: function(frm) { + frm.toggle_display(['completion_date', 'repair_status'], !(frm.doc.__islocal)); + }, + repair_status: (frm) => { if (frm.doc.completion_date && frm.doc.repair_status == "Completed") { frappe.call ({ From 30c4a56491f9aa86f491ced75d0d2eb70f302d3b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 18:43:36 +0530 Subject: [PATCH 006/376] feat(Asset Repair): Change Asset's status according to repair_status --- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 049b931b5e8..884dc195882 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -13,6 +13,10 @@ class AssetRepair(Document): if self.repair_status == "Completed" and not self.completion_date: frappe.throw(_("Please select Completion Date for Completed Repair")) + if self.repair_status == 'Pending': + frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') + else: + frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') @frappe.whitelist() def get_downtime(failure_date, completion_date): From deadcd9e97c201bff3d36b237c47c2054a7c2297 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 21:43:56 +0530 Subject: [PATCH 007/376] feat(Asset Repair): Add payable_account field --- .../doctype/asset_repair/asset_repair.json | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 853534eb319..4ed9916392a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -7,9 +7,9 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "asset", "naming_series", "column_break_2", - "asset", "asset_name", "section_break_5", "failure_date", @@ -18,7 +18,10 @@ "column_break_6", "completion_date", "repair_status", + "section_break_7", "repair_cost", + "column_break_8", + "payable_account", "section_break_9", "description", "column_break_9", @@ -151,12 +154,27 @@ "fieldname": "asset_name", "fieldtype": "Read Only", "label": "Asset Name" + }, + { + "fieldname": "payable_account", + "fieldtype": "Link", + "label": "Payable Account", + "options": "Account" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-10 22:48:42.165513", + "modified": "2021-05-11 05:11:58.330860", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 88ac9b2ec9862fcb5ed901f389baef9a8a97511c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 21:46:49 +0530 Subject: [PATCH 008/376] fix(Company): Rename 'Fixed Asset Depreciation Settings' to 'Fixed Asset Deafults' --- erpnext/setup/doctype/company/company.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 83cbf475abd..acd05c43e0e 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -74,7 +74,7 @@ "stock_received_but_not_billed", "service_received_but_not_billed", "expenses_included_in_valuation", - "fixed_asset_depreciation_settings", + "fixed_asset_defaults", "accumulated_depreciation_account", "depreciation_expense_account", "series_for_depreciation_entry", @@ -520,12 +520,6 @@ "no_copy": 1, "options": "Account" }, - { - "collapsible": 1, - "fieldname": "fixed_asset_depreciation_settings", - "fieldtype": "Section Break", - "label": "Fixed Asset Depreciation Settings" - }, { "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", @@ -740,6 +734,12 @@ "fieldtype": "Link", "label": "Default Payment Discount Account", "options": "Account" + }, + { + "collapsible": 1, + "fieldname": "fixed_asset_defaults", + "fieldtype": "Section Break", + "label": "Fixed Asset Defaults" } ], "icon": "fa fa-building", @@ -747,7 +747,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-02-16 15:53:37.167589", + "modified": "2021-05-11 21:45:22.803065", "modified_by": "Administrator", "module": "Setup", "name": "Company", From 11594f870ea58ba0984956acd6d4abd9f7183948 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 21:47:58 +0530 Subject: [PATCH 009/376] fix(Company): Add 'Repair and Maintenance Account' field --- erpnext/setup/doctype/company/company.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index acd05c43e0e..0bc85ea0929 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -83,6 +83,7 @@ "disposal_account", "depreciation_cost_center", "capital_work_in_progress_account", + "repair_and_maintenance_account", "asset_received_but_not_billed", "budget_detail", "exception_budget_approver_role", @@ -740,6 +741,11 @@ "fieldname": "fixed_asset_defaults", "fieldtype": "Section Break", "label": "Fixed Asset Defaults" + }, + { + "fieldname": "repair_and_maintenance_account", + "fieldtype": "Data", + "label": "Repair and Maintenance Account" } ], "icon": "fa fa-building", @@ -747,7 +753,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-05-11 21:45:22.803065", + "modified": "2021-05-11 21:47:04.667950", "modified_by": "Administrator", "module": "Setup", "name": "Company", From 1b2a5d1489d4e034dc04d0970b253cc1b6b89b3e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 23:33:07 +0530 Subject: [PATCH 010/376] feat(Asset Repair): Add 'Capitalize Repair Cost' checkbox --- .../doctype/asset_repair/asset_repair.json | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 4ed9916392a..8762451dd9a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -18,8 +18,9 @@ "column_break_6", "completion_date", "repair_status", - "section_break_7", + "accounting_details", "repair_cost", + "capitalize_repair_cost", "column_break_8", "payable_account", "section_break_9", @@ -161,20 +162,26 @@ "label": "Payable Account", "options": "Account" }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, { "fieldname": "column_break_8", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "capitalize_repair_cost", + "fieldtype": "Check", + "label": "Capitalize Repair Cost" + }, + { + "fieldname": "accounting_details", + "fieldtype": "Section Break", + "label": "Accounting Details" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-11 05:11:58.330860", + "modified": "2021-05-11 23:25:48.956382", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 194a08e4ec1fbff44da24d653182ce27217368b5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 23:34:21 +0530 Subject: [PATCH 011/376] fix(Asset Repair): Hide 'Accounting Details' section till doc gets saved --- .../assets/doctype/asset_repair/asset_repair.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index f5eeeda5fb1..23bc7b18918 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,8 +2,23 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { + // setup: function(frm) { + // frm.add_fetch("company", "repair_and_maintenance_account", "payable_account"); + + // frm.set_query("payable_account", function() { + // return { + // filters: { + // "report_type": "Balance Sheet", + // "account_type": "Payable", + // "company": frm.doc.company, + // "is_group": 0 + // } + // }; + // }); + // }, + refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details'], !(frm.doc.__islocal)); }, repair_status: (frm) => { From 6d1cf76b77c1264e67085e2d5d08829e0b8afb67 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 23:56:34 +0530 Subject: [PATCH 012/376] feat(Asset): Add 'Asset Value' field --- erpnext/assets/doctype/asset/asset.json | 9 ++++++++- erpnext/assets/doctype/asset/asset.py | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 421b9a6c378..4960c7e7095 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -23,6 +23,7 @@ "asset_name", "asset_category", "location", + "asset_value", "custodian", "department", "disposal_date", @@ -480,6 +481,12 @@ "fieldname": "section_break_36", "fieldtype": "Section Break", "label": "Finance Books" + }, + { + "fieldname": "asset_value", + "fieldtype": "Currency", + "label": "Asset Value", + "read_only": 1 } ], "idx": 72, @@ -502,7 +509,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-01-22 12:38:59.091510", + "modified": "2021-05-11 23:47:15.831720", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index b9f413854f7..350220b8979 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -96,6 +96,9 @@ class Asset(AccountsController): finance_books = get_item_details(self.item_code, self.asset_category) self.set('finance_books', finance_books) + if not(self.asset_value): + self.asset_value = self.gross_purchase_amount + def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") From 258c2385836a844282aa657b2c21de4550b8752f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 00:15:18 +0530 Subject: [PATCH 013/376] feat(Asset Repair): Add repair_cost to asset_value --- .../doctype/asset_repair/asset_repair.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 884dc195882..b233358e80e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -7,6 +7,9 @@ import frappe from frappe import _ from frappe.utils import time_diff_in_hours from frappe.model.document import Document +from frappe.utils import flt +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.controllers.accounts_controller import AccountsController class AssetRepair(Document): def validate(self): @@ -18,6 +21,73 @@ class AssetRepair(Document): else: frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') + def on_submit(self): + self.increase_asset_value() + + def increase_asset_value(self): + if self.capitalize_repair_cost: + asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + + # self.make_gl_entries() + + # def on_cancel(self): + # if self.payable_account: + # self.make_gl_entries(cancel=True) + + # def make_gl_entries(self, cancel=False): + # if flt(self.repair_cost) > 0: + # gl_entries = self.get_gl_entries() + # make_gl_entries(gl_entries, cancel) + + # def get_gl_entries(self): + # gl_entry = [] + # company = frappe.db.get_value('Asset', self.asset, 'company') + # repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') + + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": self.payable_account, + # "credit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": repair_and_maintenance_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name + # }) + # gl_entry.insert() + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": repair_and_maintenance_account, + # "debit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": self.payable_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name + # }) + # gl_entry.insert() + + # gl_entry.append( + # self.get_gl_dict({ + # "account": self.payable_account, + # "credit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": repair_and_maintenance_account, + # "against_voucher_type": self.doctype, + # "against_voucher": self.name + # }) + # ) + + # gl_entry.append( + # self.get_gl_dict({ + # "account": repair_and_maintenance_account, + # "debit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": self.payable_account, + # "against_voucher_type": self.doctype, + # "against_voucher": self.name + # }) + # ) + @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) From c62890bcdbabd610497c76f8ddacd60f12222ef1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 03:27:55 +0530 Subject: [PATCH 014/376] feat: Create 'Stock Item' child table --- erpnext/assets/doctype/stock_item/__init__.py | 0 .../assets/doctype/stock_item/stock_item.json | 55 +++++++++++++++++++ .../assets/doctype/stock_item/stock_item.py | 8 +++ 3 files changed, 63 insertions(+) create mode 100644 erpnext/assets/doctype/stock_item/__init__.py create mode 100644 erpnext/assets/doctype/stock_item/stock_item.json create mode 100644 erpnext/assets/doctype/stock_item/stock_item.py diff --git a/erpnext/assets/doctype/stock_item/__init__.py b/erpnext/assets/doctype/stock_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/stock_item/stock_item.json b/erpnext/assets/doctype/stock_item/stock_item.json new file mode 100644 index 00000000000..b1f05db3950 --- /dev/null +++ b/erpnext/assets/doctype/stock_item/stock_item.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2021-05-12 02:41:54.161024", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item", + "valuation_rate", + "consumed_quantity", + "total_value" + ], + "fields": [ + { + "fieldname": "item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item" + }, + { + "fetch_from": "item.valuation_rate", + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Valuation Rate", + "read_only": 1 + }, + { + "fieldname": "consumed_quantity", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumed Quantity" + }, + { + "fieldname": "total_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Value", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-12 03:19:55.006300", + "modified_by": "Administrator", + "module": "Assets", + "name": "Stock Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/stock_item/stock_item.py b/erpnext/assets/doctype/stock_item/stock_item.py new file mode 100644 index 00000000000..0e3cc3f8ba7 --- /dev/null +++ b/erpnext/assets/doctype/stock_item/stock_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class StockItem(Document): + pass From d2d31fd16424268140dc75c7611774bbf8cb5dab Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 03:28:45 +0530 Subject: [PATCH 015/376] feat(Asset Repair): Add 'Stock Items' table --- .../doctype/asset_repair/asset_repair.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 8762451dd9a..e82c5f2926f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -23,11 +23,13 @@ "capitalize_repair_cost", "column_break_8", "payable_account", + "section_break_17", + "stock_items", "section_break_9", "description", "column_break_9", "actions_performed", - "section_break_17", + "section_break_23", "downtime", "column_break_19", "amended_from" @@ -176,12 +178,22 @@ "fieldname": "accounting_details", "fieldtype": "Section Break", "label": "Accounting Details" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "options": "Stock Item" + }, + { + "fieldname": "section_break_23", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-11 23:25:48.956382", + "modified": "2021-05-12 03:23:30.618741", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 0739d4bb1b3c79734f1eec03194b504fb7886888 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 05:15:26 +0530 Subject: [PATCH 016/376] feat(Asset Repair): Add value of consumed stock items to asset value --- .../assets/doctype/asset_repair/asset_repair.js | 11 ++++++++++- .../doctype/asset_repair/asset_repair.json | 12 ++++++------ .../assets/doctype/asset_repair/asset_repair.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 23bc7b18918..4641f3aaa28 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -17,8 +17,17 @@ frappe.ui.form.on('Asset Repair', { // }); // }, + // stock_items_add: function(frm){ + // var table = frm.doc.stock_items; + // for(var i in table) { + // if (table[i].valuation_rate == 0) { + // frm.set_value(table[i].total_value, (table[i].valuation_rate * table[i].consumed_quantity)) + // } + // } + // }, + refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section'], !(frm.doc.__islocal)); }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index e82c5f2926f..a8a9f2d195b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -23,7 +23,7 @@ "capitalize_repair_cost", "column_break_8", "payable_account", - "section_break_17", + "stock_items_section", "stock_items", "section_break_9", "description", @@ -113,10 +113,6 @@ "fieldtype": "Long Text", "label": "Actions performed" }, - { - "fieldname": "section_break_17", - "fieldtype": "Section Break" - }, { "allow_on_submit": 1, "fieldname": "downtime", @@ -188,12 +184,16 @@ { "fieldname": "section_break_23", "fieldtype": "Section Break" + }, + { + "fieldname": "stock_items_section", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 03:23:30.618741", + "modified": "2021-05-12 04:45:38.714776", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index b233358e80e..0980ddfa7f2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -16,17 +16,33 @@ class AssetRepair(Document): if self.repair_status == "Completed" and not self.completion_date: frappe.throw(_("Please select Completion Date for Completed Repair")) + self.update_status() + self.set_total_value() + + def set_total_value(self): + for item in self.stock_items: + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + + def update_status(self): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') def on_submit(self): + if self.repair_status == "Pending": + frappe.throw(_("Please update Repair Status.")) + self.increase_asset_value() def increase_asset_value(self): if self.capitalize_repair_cost: asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost + for item in self.stock_items: + asset_value += item.total_value + + print("*" * 20) + print(asset_value) frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) # self.make_gl_entries() From e5ab5d8963efdc6848adf935eea122341aa1295c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 20:39:53 +0530 Subject: [PATCH 017/376] feat(Asset Repair): Create GL Entries --- .../doctype/asset_repair/asset_repair.py | 89 +++++++------------ erpnext/setup/doctype/company/company.json | 7 +- 2 files changed, 38 insertions(+), 58 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 0980ddfa7f2..d34970d16d0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -34,6 +34,7 @@ class AssetRepair(Document): frappe.throw(_("Please update Repair Status.")) self.increase_asset_value() + self.make_gl_entries() def increase_asset_value(self): if self.capitalize_repair_cost: @@ -45,64 +46,42 @@ class AssetRepair(Document): print(asset_value) frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) - # self.make_gl_entries() + def on_cancel(self): + if self.payable_account: + self.make_gl_entries(cancel=True) - # def on_cancel(self): - # if self.payable_account: - # self.make_gl_entries(cancel=True) + def make_gl_entries(self, cancel=False): + if flt(self.repair_cost) > 0: + gl_entries = self.get_gl_entries() + make_gl_entries(gl_entries, cancel) - # def make_gl_entries(self, cancel=False): - # if flt(self.repair_cost) > 0: - # gl_entries = self.get_gl_entries() - # make_gl_entries(gl_entries, cancel) + def get_gl_entries(self): + gl_entry = [] + company = frappe.db.get_value('Asset', self.asset, 'company') + repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') - # def get_gl_entries(self): - # gl_entry = [] - # company = frappe.db.get_value('Asset', self.asset, 'company') - # repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') - - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": self.payable_account, - # "credit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": repair_and_maintenance_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name - # }) - # gl_entry.insert() - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": repair_and_maintenance_account, - # "debit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": self.payable_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name - # }) - # gl_entry.insert() - - # gl_entry.append( - # self.get_gl_dict({ - # "account": self.payable_account, - # "credit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": repair_and_maintenance_account, - # "against_voucher_type": self.doctype, - # "against_voucher": self.name - # }) - # ) - - # gl_entry.append( - # self.get_gl_dict({ - # "account": repair_and_maintenance_account, - # "debit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": self.payable_account, - # "against_voucher_type": self.doctype, - # "against_voucher": self.name - # }) - # ) + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": self.payable_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": "Main - F" + }) + gl_entry.insert() + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": repair_and_maintenance_account, + "debit": self.repair_cost, + "debit_in_account_currency": self.repair_cost, + "against": self.payable_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": "Main - F" + }) + gl_entry.insert() @frappe.whitelist() def get_downtime(failure_date, completion_date): diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 0bc85ea0929..51757f584eb 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -744,8 +744,9 @@ }, { "fieldname": "repair_and_maintenance_account", - "fieldtype": "Data", - "label": "Repair and Maintenance Account" + "fieldtype": "Link", + "label": "Repair and Maintenance Account", + "options": "Account" } ], "icon": "fa fa-building", @@ -753,7 +754,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-05-11 21:47:04.667950", + "modified": "2021-05-12 16:51:08.187233", "modified_by": "Administrator", "module": "Setup", "name": "Company", From f5a2ea9cb370727e9e1f5e3da2cf057155872f4d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:06:20 +0530 Subject: [PATCH 018/376] feat(Asset Repair): Add 'Accounting Dimensions' section --- .../doctype/asset_repair/asset_repair.js | 2 +- .../doctype/asset_repair/asset_repair.json | 27 ++++++++++++++++++- .../doctype/asset_repair/asset_repair.py | 4 +-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 4641f3aaa28..d61cd83e3e7 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -27,7 +27,7 @@ frappe.ui.form.on('Asset Repair', { // }, refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section', 'accounting_dimensions_section'], !(frm.doc.__islocal)); }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index a8a9f2d195b..18bb2da6589 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -18,6 +18,10 @@ "column_break_6", "completion_date", "repair_status", + "accounting_dimensions_section", + "cost_center", + "column_break_14", + "project", "accounting_details", "repair_cost", "capitalize_repair_cost", @@ -188,12 +192,33 @@ { "fieldname": "stock_items_section", "fieldtype": "Section Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 04:45:38.714776", + "modified": "2021-05-12 21:03:59.032864", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d34970d16d0..e4cbabee369 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -41,9 +41,7 @@ class AssetRepair(Document): asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost for item in self.stock_items: asset_value += item.total_value - - print("*" * 20) - print(asset_value) + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) def on_cancel(self): From d554267b0567ef7e70a47708793fdab7aaaa30a5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:08:55 +0530 Subject: [PATCH 019/376] feat(Asset Repair): Add 'Cost Center' to GL Entries --- erpnext/assets/doctype/asset_repair/asset_repair.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index e4cbabee369..343e6c4c9cf 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -41,7 +41,7 @@ class AssetRepair(Document): asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost for item in self.stock_items: asset_value += item.total_value - + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) def on_cancel(self): @@ -66,7 +66,7 @@ class AssetRepair(Document): "against": repair_and_maintenance_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": "Main - F" + "cost_center": self.cost_center }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -77,7 +77,7 @@ class AssetRepair(Document): "against": self.payable_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": "Main - F" + "cost_center": self.cost_center }) gl_entry.insert() From 234b473d9316ed3c536821a5079cb9d293118bf4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:10:16 +0530 Subject: [PATCH 020/376] feat(Asset Repair): Remove 'Assign To' --- .../doctype/asset_repair/asset_repair.json | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 18bb2da6589..cda0edf1c59 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -13,8 +13,6 @@ "asset_name", "section_break_5", "failure_date", - "assign_to", - "assign_to_name", "column_break_6", "completion_date", "repair_status", @@ -62,20 +60,6 @@ "label": "Failure Date", "reqd": 1 }, - { - "allow_on_submit": 1, - "fieldname": "assign_to", - "fieldtype": "Link", - "label": "Assign To", - "options": "User" - }, - { - "allow_on_submit": 1, - "fetch_from": "assign_to.full_name", - "fieldname": "assign_to_name", - "fieldtype": "Read Only", - "label": "Assign To Name" - }, { "fieldname": "column_break_6", "fieldtype": "Column Break" @@ -218,7 +202,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 21:03:59.032864", + "modified": "2021-05-12 21:09:27.994356", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 7ad74cf8004092fe1be44f455bdcdafcbaecfa7f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:22:36 +0530 Subject: [PATCH 021/376] feat(Asset Repair): Add stock_consumption checkbox --- .../doctype/asset_repair/asset_repair.js | 3 ++- .../doctype/asset_repair/asset_repair.json | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index d61cd83e3e7..605edccc115 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -27,7 +27,8 @@ frappe.ui.form.on('Asset Repair', { // }, refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section', 'accounting_dimensions_section'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); + frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index cda0edf1c59..9ab9271fa96 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -23,9 +23,10 @@ "accounting_details", "repair_cost", "capitalize_repair_cost", + "stock_consumption", "column_break_8", "payable_account", - "stock_items_section", + "stock_consumption_details_section", "stock_items", "section_break_9", "description", @@ -173,10 +174,6 @@ "fieldname": "section_break_23", "fieldtype": "Section Break" }, - { - "fieldname": "stock_items_section", - "fieldtype": "Section Break" - }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", @@ -197,12 +194,23 @@ { "fieldname": "column_break_14", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "stock_consumption", + "fieldtype": "Check", + "label": "Stock Consumed During Repair" + }, + { + "fieldname": "stock_consumption_details_section", + "fieldtype": "Section Break", + "label": "Stock Consumption Details" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 21:09:27.994356", + "modified": "2021-05-12 21:20:18.276755", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From c2b0102852acc1d826fdf9bc01ca7c9a75dbca6b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 00:58:07 +0530 Subject: [PATCH 022/376] feat(Asset Repair): Add 'View General Ledger' button --- erpnext/assets/doctype/asset_repair/asset_repair.js | 9 +++++++++ erpnext/assets/doctype/asset_repair/asset_repair.py | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 605edccc115..a5fda53429e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -29,6 +29,15 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) + + if (frm.doc.docstatus) { + frm.add_custom_button("View General Ledger", function() { + frappe.route_options = { + "voucher_no": frm.doc.name + }; + frappe.set_route("query-report", "General Ledger"); + }); + } }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 343e6c4c9cf..9c3d880eace 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,11 +5,10 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours +from frappe.utils import time_diff_in_hours, getdate from frappe.model.document import Document from frappe.utils import flt from erpnext.accounts.general_ledger import make_gl_entries -from erpnext.controllers.accounts_controller import AccountsController class AssetRepair(Document): def validate(self): @@ -66,7 +65,8 @@ class AssetRepair(Document): "against": repair_and_maintenance_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": self.cost_center + "cost_center": self.cost_center, + "posting_date": getdate() }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -77,7 +77,8 @@ class AssetRepair(Document): "against": self.payable_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": self.cost_center + "cost_center": self.cost_center, + "posting_date": getdate() }) gl_entry.insert() From 385ca80ceb939fe76fecb5fcfc95742345729b04 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 02:53:57 +0530 Subject: [PATCH 023/376] feat(Asset Repair): Add total_repair_cost field --- .../doctype/asset_repair/asset_repair.js | 2 +- .../doctype/asset_repair/asset_repair.json | 8 ++- .../doctype/asset_repair/asset_repair.py | 60 +++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index a5fda53429e..9d06caeb982 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -28,7 +28,7 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) + frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption) if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 9ab9271fa96..df9ab7d63b2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -26,6 +26,7 @@ "stock_consumption", "column_break_8", "payable_account", + "total_repair_cost", "stock_consumption_details_section", "stock_items", "section_break_9", @@ -205,12 +206,17 @@ "fieldname": "stock_consumption_details_section", "fieldtype": "Section Break", "label": "Stock Consumption Details" + }, + { + "fieldname": "total_repair_cost", + "fieldtype": "Currency", + "label": "Total Repair Cost" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 21:20:18.276755", + "modified": "2021-05-13 02:40:57.953076", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9c3d880eace..2b69b5b1105 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -16,18 +16,31 @@ class AssetRepair(Document): frappe.throw(_("Please select Completion Date for Completed Repair")) self.update_status() - self.set_total_value() - - def set_total_value(self): - for item in self.stock_items: - item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) - + self.set_total_value() # change later + self.check_stock_items() + self.calculate_total_repair_cost() + def update_status(self): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') + def set_total_value(self): + for item in self.stock_items: + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + + def check_stock_items(self): + if self.stock_consumption: + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + + def calculate_total_repair_cost(self): + self.total_repair_cost = self.repair_cost + if self.stock_consumption: + for item in self.stock_items: + self.total_repair_cost += item.total_value + def on_submit(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) @@ -81,6 +94,41 @@ class AssetRepair(Document): "posting_date": getdate() }) gl_entry.insert() + + # if self.capitalize_repair_cost: + # fixed_asset_account = self.get_fixed_asset_account() + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": self.payable_account, + # "credit": self.total_repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": repair_and_maintenance_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name, + # "cost_center": self.cost_center, + # "posting_date": getdate() + # }) + # gl_entry.insert() + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": fixed_asset_account, + # "debit": self.total_repair_cost, + # "debit_in_account_currency": self.repair_cost, + # "against": self.payable_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name, + # "cost_center": self.cost_center, + # "posting_date": getdate() + # }) + # gl_entry.insert() + + # def get_fixed_asset_account(self): + # asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) + # company = frappe.db.get_value('Asset', self.asset, 'company') + # for account in asset_category.accounts: + # if account.company_name == company: + # return account.fixed_asset_account + @frappe.whitelist() def get_downtime(failure_date, completion_date): From 8d05eca45e8e3dc6ed17d0471d278ef4f9226a39 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 03:26:03 +0530 Subject: [PATCH 024/376] feat(Asset Repair): Create GL Entries to capitalise repair cost --- .../doctype/asset_repair/asset_repair.py | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 2b69b5b1105..12dda1d4a1e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -73,8 +73,8 @@ class AssetRepair(Document): gl_entry = frappe.get_doc({ "doctype": "GL Entry", "account": self.payable_account, - "credit": self.repair_cost, - "credit_in_account_currency": self.repair_cost, + "credit": self.total_repair_cost, + "credit_in_account_currency": self.total_repair_cost, "against": repair_and_maintenance_account, "voucher_type": self.doctype, "voucher_no": self.name, @@ -85,8 +85,8 @@ class AssetRepair(Document): gl_entry = frappe.get_doc({ "doctype": "GL Entry", "account": repair_and_maintenance_account, - "debit": self.repair_cost, - "debit_in_account_currency": self.repair_cost, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, "against": self.payable_account, "voucher_type": self.doctype, "voucher_no": self.name, @@ -95,39 +95,39 @@ class AssetRepair(Document): }) gl_entry.insert() - # if self.capitalize_repair_cost: - # fixed_asset_account = self.get_fixed_asset_account() - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": self.payable_account, - # "credit": self.total_repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": repair_and_maintenance_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name, - # "cost_center": self.cost_center, - # "posting_date": getdate() - # }) - # gl_entry.insert() - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": fixed_asset_account, - # "debit": self.total_repair_cost, - # "debit_in_account_currency": self.repair_cost, - # "against": self.payable_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name, - # "cost_center": self.cost_center, - # "posting_date": getdate() - # }) - # gl_entry.insert() + if self.capitalize_repair_cost: + fixed_asset_account = self.get_fixed_asset_account() + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": self.payable_account, + "credit": self.total_repair_cost, + "credit_in_account_currency": self.total_repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": "Asset", + "voucher_no": self.asset, + "cost_center": self.cost_center, + "posting_date": getdate() + }) + gl_entry.insert() + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": fixed_asset_account, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, + "against": self.payable_account, + "voucher_type": "Asset", + "voucher_no": self.asset, + "cost_center": self.cost_center, + "posting_date": getdate() + }) + gl_entry.insert() - # def get_fixed_asset_account(self): - # asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) - # company = frappe.db.get_value('Asset', self.asset, 'company') - # for account in asset_category.accounts: - # if account.company_name == company: - # return account.fixed_asset_account + def get_fixed_asset_account(self): + asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) + company = frappe.db.get_value('Asset', self.asset, 'company') + for account in asset_category.accounts: + if account.company_name == company: + return account.fixed_asset_account @frappe.whitelist() From f2fe55ceeb608c34e4a8a0ab73d2511e36603dee Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 03:42:32 +0530 Subject: [PATCH 025/376] feat(Asset Repair): Check for Cost Center and Payable Account --- .../assets/doctype/asset_repair/asset_repair.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 12dda1d4a1e..dcad1c61810 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -42,12 +42,25 @@ class AssetRepair(Document): self.total_repair_cost += item.total_value def on_submit(self): - if self.repair_status == "Pending": - frappe.throw(_("Please update Repair Status.")) + self.check_repair_status() + self.check_for_payable_account() + self.check_for_cost_center() self.increase_asset_value() self.make_gl_entries() + def check_repair_status(self): + if self.repair_status == "Pending": + frappe.throw(_("Please update Repair Status.")) + + def check_for_payable_account(self): + if not self.payable_account: + frappe.throw(_("Please enter Payable Account.")) + + def check_for_cost_center(self): + if not self.cost_center: + frappe.throw(_("Please enter Cost Center.")) + def increase_asset_value(self): if self.capitalize_repair_cost: asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost From c8cb96a38fb811d9a7f1a8355d5071726087be49 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 04:04:52 +0530 Subject: [PATCH 026/376] feat(Asset Repair): Add Warehouse field --- .../assets/doctype/asset_repair/asset_repair.json | 9 ++++++++- erpnext/assets/doctype/asset_repair/asset_repair.py | 12 ++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index df9ab7d63b2..ed7708c8dfa 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -28,6 +28,7 @@ "payable_account", "total_repair_cost", "stock_consumption_details_section", + "warehouse", "stock_items", "section_break_9", "description", @@ -211,12 +212,18 @@ "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 02:40:57.953076", + "modified": "2021-05-13 03:50:39.146322", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index dcad1c61810..ed022fd8081 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -17,7 +17,6 @@ class AssetRepair(Document): self.update_status() self.set_total_value() # change later - self.check_stock_items() self.calculate_total_repair_cost() def update_status(self): @@ -30,11 +29,6 @@ class AssetRepair(Document): for item in self.stock_items: item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) - def check_stock_items(self): - if self.stock_consumption: - if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) - def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost if self.stock_consumption: @@ -43,6 +37,7 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() + self.check_stock_items() self.check_for_payable_account() self.check_for_cost_center() @@ -53,6 +48,11 @@ class AssetRepair(Document): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) + def check_stock_items(self): + if self.stock_consumption: + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + def check_for_payable_account(self): if not self.payable_account: frappe.throw(_("Please enter Payable Account.")) From 1ac1cedfff947095879d032be1d74d433aca4c2d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 04:45:21 +0530 Subject: [PATCH 027/376] feat(Asset Repair): Decrease stock quantity if consumed during Asset Repair --- .../doctype/asset_repair/asset_repair.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ed022fd8081..3b7587e61fa 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -37,21 +37,25 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_stock_items() + self.check_for_stock_items_and_warehouse() self.check_for_payable_account() self.check_for_cost_center() self.increase_asset_value() + if self.stock_consumption: + self.decrease_stock_quantity() self.make_gl_entries() def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) - def check_stock_items(self): + def check_for_stock_items_and_warehouse(self): if self.stock_consumption: if not self.stock_items: frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + if not self.warehouse: + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) def check_for_payable_account(self): if not self.payable_account: @@ -68,6 +72,22 @@ class AssetRepair(Document): asset_value += item.total_value frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + + def decrease_stock_quantity(self): + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Issue" + }) + + for stock_item in self.stock_items: + stock_entry.append('items', { + "s_warehouse": self.warehouse, + "item_code": stock_item.item, + "qty": stock_item.consumed_quantity + }) + + stock_entry.insert() + stock_entry.submit() def on_cancel(self): if self.payable_account: From ec72f8956f5c25dda1d19cd7691ac0a8349bae60 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:02:10 +0530 Subject: [PATCH 028/376] fix(Asset Repair): Revert to asset's previous status if repair is completed/cancelled --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3b7587e61fa..975e3674a43 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -23,7 +23,8 @@ class AssetRepair(Document): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: - frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') + asset = frappe.get_doc('Asset', self.asset) + asset.set_status() def set_total_value(self): for item in self.stock_items: From 68b78374b50ed09f83bb59103deeede2da416a6e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:03:56 +0530 Subject: [PATCH 029/376] fix(Asset): Make Manage button white --- erpnext/assets/doctype/asset/asset.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 2a57183a802..1e67ec816b6 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -132,7 +132,6 @@ frappe.ui.form.on('Asset', { }, __("Manage")); } - frm.page.set_inner_btn_group_as_primary(__("Manage")); frm.trigger("setup_chart"); } From f7eebf0e782f1e3feb82df0feacadcd8306b938b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:08:33 +0530 Subject: [PATCH 030/376] feat(Asset Repair): Add 'Increase In Asset Life' field --- .../assets/doctype/asset_repair/asset_repair.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index ed7708c8dfa..26f1c6d7cc4 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -30,6 +30,8 @@ "stock_consumption_details_section", "warehouse", "stock_items", + "asset_depreciation_details_section", + "increase_in_asset_life", "section_break_9", "description", "column_break_9", @@ -218,12 +220,22 @@ "fieldtype": "Link", "label": "Warehouse", "options": "Warehouse" + }, + { + "fieldname": "asset_depreciation_details_section", + "fieldtype": "Section Break", + "label": "Asset Depreciation Details" + }, + { + "fieldname": "increase_in_asset_life", + "fieldtype": "Int", + "label": "Increase In Asset Life" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 03:50:39.146322", + "modified": "2021-05-13 05:08:00.676616", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 2874d0853401d3b0570534d1c6dee34885ded1c0 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:22:35 +0530 Subject: [PATCH 031/376] fix(Asset Repair): Rearrange fields --- erpnext/assets/doctype/asset_repair/asset_repair.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 26f1c6d7cc4..13f4610f8ec 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -13,19 +13,19 @@ "asset_name", "section_break_5", "failure_date", + "repair_status", "column_break_6", "completion_date", - "repair_status", "accounting_dimensions_section", "cost_center", "column_break_14", "project", "accounting_details", - "repair_cost", + "payable_account", "capitalize_repair_cost", "stock_consumption", "column_break_8", - "payable_account", + "repair_cost", "total_repair_cost", "stock_consumption_details_section", "warehouse", @@ -235,7 +235,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 05:08:00.676616", + "modified": "2021-05-13 05:11:42.550016", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 568369a5c20488d23cb61974f3d439e4603b756a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:23:21 +0530 Subject: [PATCH 032/376] feat(Asset Maintenance): Add stock_consumption checkbox --- .../doctype/asset_maintenance/asset_maintenance.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index c0c2566fe23..669f1959e59 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -12,6 +12,7 @@ "column_break_3", "item_code", "item_name", + "stock_consumption", "section_break_6", "maintenance_team", "column_break_9", @@ -100,10 +101,16 @@ "label": "Maintenance Tasks", "options": "Asset Maintenance Task", "reqd": 1 + }, + { + "default": "0", + "fieldname": "stock_consumption", + "fieldtype": "Check", + "label": "Stock Consumed During Maintenance" } ], "links": [], - "modified": "2020-05-28 20:28:32.993823", + "modified": "2021-05-13 05:20:45.809270", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", From 04a909bd994667a3eb1cdcd0c5223a214c7ece0d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:27:15 +0530 Subject: [PATCH 033/376] feat(Asset Maintenance): Add 'Stock Consumption Details' section --- .../asset_maintenance/asset_maintenance.js | 3 +++ .../asset_maintenance/asset_maintenance.json | 24 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 70b8654509f..3830d1168c4 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -30,7 +30,10 @@ frappe.ui.form.on('Asset Maintenance', { if(!frm.is_new()) { frm.trigger('make_dashboard'); } + + frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) }, + make_dashboard: (frm) => { if(!frm.is_new()) { frappe.call({ diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index 669f1959e59..da2fd754512 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -19,7 +19,10 @@ "maintenance_manager", "maintenance_manager_name", "section_break_8", - "asset_maintenance_tasks" + "asset_maintenance_tasks", + "stock_consumption_details_section", + "warehouse", + "stock_items" ], "fields": [ { @@ -107,10 +110,27 @@ "fieldname": "stock_consumption", "fieldtype": "Check", "label": "Stock Consumed During Maintenance" + }, + { + "fieldname": "stock_consumption_details_section", + "fieldtype": "Section Break", + "label": "Stock Consumption Details" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "options": "Stock Item" } ], "links": [], - "modified": "2021-05-13 05:20:45.809270", + "modified": "2021-05-13 05:24:58.480132", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", From d1f521701cf7a60b3e90ac81b1609432af8baca6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:33:19 +0530 Subject: [PATCH 034/376] feat(Asset Maintenance): Check if Warehouse and Stock Items were entered --- .../doctype/asset_maintenance/asset_maintenance.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index a506deec93e..8bc7ea1b70e 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -24,6 +24,16 @@ class AssetMaintenance(Document): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) self.sync_maintenance_tasks() + def on_submit(self): + self.check_for_stock_items_and_warehouse() + + def check_for_stock_items_and_warehouse(self): + if self.stock_consumption: + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Maintenance.")) + if not self.warehouse: + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) + def sync_maintenance_tasks(self): tasks_names = [] for task in self.get('asset_maintenance_tasks'): From 70bad470f7116c9ace21373aed205f8e7735c235 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:35:02 +0530 Subject: [PATCH 035/376] feat(Asset Maintenance): Increase Asset value if Stock Items were consumed --- .../assets/doctype/asset_maintenance/asset_maintenance.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 8bc7ea1b70e..ecd55e38f21 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -26,6 +26,7 @@ class AssetMaintenance(Document): def on_submit(self): self.check_for_stock_items_and_warehouse() + self.increase_asset_value() def check_for_stock_items_and_warehouse(self): if self.stock_consumption: @@ -34,6 +35,13 @@ class AssetMaintenance(Document): if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) + def increase_asset_value(self): + asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + for item in self.stock_items: + asset_value += item.total_value + + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + def sync_maintenance_tasks(self): tasks_names = [] for task in self.get('asset_maintenance_tasks'): From a7bbaacfde57076d999f96577828698a41bad49a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:38:54 +0530 Subject: [PATCH 036/376] fix(Asset Repair): Always add value of consumed Stock Items to Asset value --- erpnext/assets/doctype/asset_repair/asset_repair.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 975e3674a43..3ac6f26f6c2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -67,12 +67,13 @@ class AssetRepair(Document): frappe.throw(_("Please enter Cost Center.")) def increase_asset_value(self): - if self.capitalize_repair_cost: - asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost - for item in self.stock_items: - asset_value += item.total_value + asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + for item in self.stock_items: + asset_value += item.total_value - frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + if self.capitalize_repair_cost: + asset_value += self.repair_cost + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ From 15294d5543262d69a433861e67bf50042368d8f1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:40:56 +0530 Subject: [PATCH 037/376] feat(Asset Maintenance): Decrease stock quantity if consumed during Asset Maintenance --- .../asset_maintenance/asset_maintenance.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index ecd55e38f21..e3e654c398b 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -19,14 +19,15 @@ class AssetMaintenance(Document): if not task.assign_to and self.docstatus == 0: throw(_("Row #{}: Please asign task to a member.").format(task.idx)) + if self.stock_consumption: + self.check_for_stock_items_and_warehouse() + self.increase_asset_value() + self.decrease_stock_quantity() + def on_update(self): for task in self.get('asset_maintenance_tasks'): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) - self.sync_maintenance_tasks() - - def on_submit(self): - self.check_for_stock_items_and_warehouse() - self.increase_asset_value() + self.sync_maintenance_tasks() def check_for_stock_items_and_warehouse(self): if self.stock_consumption: @@ -36,11 +37,27 @@ class AssetMaintenance(Document): frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) def increase_asset_value(self): - asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + asset_value = frappe.db.get_value('Asset', self.asset_name, 'asset_value') for item in self.stock_items: asset_value += item.total_value - frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + frappe.db.set_value('Asset', self.asset_name, 'asset_value', asset_value) + + def decrease_stock_quantity(self): + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Issue" + }) + + for stock_item in self.stock_items: + stock_entry.append('items', { + "s_warehouse": self.warehouse, + "item_code": stock_item.item, + "qty": stock_item.consumed_quantity + }) + + stock_entry.insert() + stock_entry.submit() def sync_maintenance_tasks(self): tasks_names = [] From d5d7cacd6712e8636f3102b0ede9796cb1217954 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:52:18 +0530 Subject: [PATCH 038/376] fix(Asset Repair): Improve code --- erpnext/assets/doctype/asset_repair/asset_repair.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3ac6f26f6c2..8ff3b796d20 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -38,12 +38,12 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_for_stock_items_and_warehouse() self.check_for_payable_account() self.check_for_cost_center() self.increase_asset_value() if self.stock_consumption: + self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() self.make_gl_entries() @@ -52,11 +52,10 @@ class AssetRepair(Document): frappe.throw(_("Please update Repair Status.")) def check_for_stock_items_and_warehouse(self): - if self.stock_consumption: - if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) - if not self.warehouse: - frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + if not self.warehouse: + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) def check_for_payable_account(self): if not self.payable_account: From 3ab852d4d00041fdd52250646d15b397d22001fa Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:56:43 +0530 Subject: [PATCH 039/376] fix(Asset Repair): Fetch 'Asset Name' when 'Asset' is selected --- erpnext/assets/doctype/asset_repair/asset_repair.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 13f4610f8ec..968809c7d5d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -143,6 +143,7 @@ "reqd": 1 }, { + "fetch_from": "asset.asset_name", "fieldname": "asset_name", "fieldtype": "Read Only", "label": "Asset Name" @@ -235,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 05:11:42.550016", + "modified": "2021-05-13 05:55:16.131448", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From a8037c1896f5bd4b0aa78c6f3e5c2409779affcf Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 23:07:19 +0530 Subject: [PATCH 040/376] fix(Asset Repair): Add Purchase Invoice --- erpnext/assets/doctype/asset_repair/asset_repair.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 968809c7d5d..2c1552f0269 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -22,11 +22,12 @@ "project", "accounting_details", "payable_account", - "capitalize_repair_cost", + "purchase_invoice", "stock_consumption", "column_break_8", "repair_cost", "total_repair_cost", + "capitalize_repair_cost", "stock_consumption_details_section", "warehouse", "stock_items", @@ -231,12 +232,18 @@ "fieldname": "increase_in_asset_life", "fieldtype": "Int", "label": "Increase In Asset Life" + }, + { + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "label": "Purchase Invoice", + "options": "Purchase Invoice" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 05:55:16.131448", + "modified": "2021-05-13 23:01:03.638835", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 66e6f01e40a0f9eaf6242d4eec7e97bc410e830f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 23:17:17 +0530 Subject: [PATCH 041/376] fix(Asset Repair): Make Purchase Invoice mandatory if capitalize_repair_cost is checked --- erpnext/assets/doctype/asset_repair/asset_repair.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 8ff3b796d20..2807c0f5bd6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -41,10 +41,13 @@ class AssetRepair(Document): self.check_for_payable_account() self.check_for_cost_center() - self.increase_asset_value() + if self.stock_consumption or self.capitalize_repair_cost: + self.increase_asset_value() if self.stock_consumption: self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() + if self.capitalize_repair_cost: + self.check_for_purchase_invoice() self.make_gl_entries() def check_repair_status(self): @@ -90,6 +93,10 @@ class AssetRepair(Document): stock_entry.insert() stock_entry.submit() + def check_for_purchase_invoice(self): + if not self.purchase_invoice: + frappe.throw(_("Please link Purchase Invoice.")) + def on_cancel(self): if self.payable_account: self.make_gl_entries(cancel=True) From 8a41f6354aa93dd1d366768a159e380a31766cdf Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 14 May 2021 02:35:13 +0530 Subject: [PATCH 042/376] fix(Asset Repair): Remove 'Payable Account' field --- .../assets/doctype/asset_repair/asset_repair.json | 15 ++++----------- .../assets/doctype/asset_repair/asset_repair.py | 8 +------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 2c1552f0269..a2d962cd959 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -21,13 +21,12 @@ "column_break_14", "project", "accounting_details", - "payable_account", - "purchase_invoice", + "repair_cost", + "capitalize_repair_cost", "stock_consumption", "column_break_8", - "repair_cost", "total_repair_cost", - "capitalize_repair_cost", + "purchase_invoice", "stock_consumption_details_section", "warehouse", "stock_items", @@ -149,12 +148,6 @@ "fieldtype": "Read Only", "label": "Asset Name" }, - { - "fieldname": "payable_account", - "fieldtype": "Link", - "label": "Payable Account", - "options": "Account" - }, { "fieldname": "column_break_8", "fieldtype": "Column Break" @@ -243,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 23:01:03.638835", + "modified": "2021-05-14 02:31:57.226273", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 2807c0f5bd6..5849c8ddd60 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -38,7 +38,6 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_for_payable_account() self.check_for_cost_center() if self.stock_consumption or self.capitalize_repair_cost: @@ -60,10 +59,6 @@ class AssetRepair(Document): if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) - def check_for_payable_account(self): - if not self.payable_account: - frappe.throw(_("Please enter Payable Account.")) - def check_for_cost_center(self): if not self.cost_center: frappe.throw(_("Please enter Cost Center.")) @@ -98,8 +93,7 @@ class AssetRepair(Document): frappe.throw(_("Please link Purchase Invoice.")) def on_cancel(self): - if self.payable_account: - self.make_gl_entries(cancel=True) + self.make_gl_entries(cancel=True) def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: From 6bb920f25e9cf4e36d37fea774d478bb1117b594 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 14 May 2021 03:20:15 +0530 Subject: [PATCH 043/376] fix(Asset Repair): Only create GL entries if repair cost is capitalised --- .../doctype/asset_repair/asset_repair.py | 43 +++++-------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 5849c8ddd60..74d3114ea48 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -47,7 +47,7 @@ class AssetRepair(Document): self.decrease_stock_quantity() if self.capitalize_repair_cost: self.check_for_purchase_invoice() - self.make_gl_entries() + self.make_gl_entries() def check_repair_status(self): if self.repair_status == "Pending": @@ -104,14 +104,16 @@ class AssetRepair(Document): gl_entry = [] company = frappe.db.get_value('Asset', self.asset, 'company') repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') + fixed_asset_account = self.get_fixed_asset_account() + expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account gl_entry = frappe.get_doc({ "doctype": "GL Entry", - "account": self.payable_account, + "account": expense_account, "credit": self.total_repair_cost, "credit_in_account_currency": self.total_repair_cost, "against": repair_and_maintenance_account, - "voucher_type": self.doctype, + "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, "posting_date": getdate() @@ -119,44 +121,19 @@ class AssetRepair(Document): gl_entry.insert() gl_entry = frappe.get_doc({ "doctype": "GL Entry", - "account": repair_and_maintenance_account, + "account": fixed_asset_account, "debit": self.total_repair_cost, "debit_in_account_currency": self.total_repair_cost, - "against": self.payable_account, + "against": expense_account, "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate() + "posting_date": getdate(), + "against_voucher_type": "Purchase Invoice", + "against_voucher": self.purchase_invoice }) gl_entry.insert() - if self.capitalize_repair_cost: - fixed_asset_account = self.get_fixed_asset_account() - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": self.payable_account, - "credit": self.total_repair_cost, - "credit_in_account_currency": self.total_repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": "Asset", - "voucher_no": self.asset, - "cost_center": self.cost_center, - "posting_date": getdate() - }) - gl_entry.insert() - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": fixed_asset_account, - "debit": self.total_repair_cost, - "debit_in_account_currency": self.total_repair_cost, - "against": self.payable_account, - "voucher_type": "Asset", - "voucher_no": self.asset, - "cost_center": self.cost_center, - "posting_date": getdate() - }) - gl_entry.insert() - def get_fixed_asset_account(self): asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) company = frappe.db.get_value('Asset', self.asset, 'company') From 30fdebafa7dcb874b499505dc8e4515ae97fd58d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 10:12:28 +0530 Subject: [PATCH 044/376] feat(Asset): Modify depreciation schedule --- erpnext/assets/doctype/asset/asset.py | 19 ++++++++++++++----- .../doctype/asset_repair/asset_repair.js | 3 ++- .../doctype/asset_repair/asset_repair.py | 11 ++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 350220b8979..f837c5e7c19 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -171,15 +171,23 @@ class Asset(AccountsController): d.precision("rate_of_depreciation")) def make_depreciation_schedule(self): - if 'Manual' not in [d.depreciation_method for d in self.finance_books]: + if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: self.schedules = [] - if self.get("schedules") or not self.available_for_use_date: + if not self.available_for_use_date: return for d in self.get('finance_books'): self.validate_asset_finance_books(d) + start = 0 + for n in range (len(self.schedules)): + if not self.schedules[n].journal_entry: + print("*"*100) + del self.schedules[n:] + start = n + break + value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -192,9 +200,9 @@ class Asset(AccountsController): if has_pro_rata: number_of_pending_depreciations += 1 - + skip_row = False - for n in range(number_of_pending_depreciations): + for n in range(start, number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -350,11 +358,12 @@ class Asset(AccountsController): if d.finance_book_id not in finance_books: accumulated_depreciation = flt(self.opening_accumulated_depreciation) value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) - finance_books.append(d.finance_book_id) + finance_books.append(int(d.finance_book_id)) depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) value_after_depreciation -= flt(depreciation_amount) + # for the last row, if depreciation method = Straight Line if straight_line_idx and i == max(straight_line_idx) - 1: book = self.get('finance_books')[cint(d.finance_book_id) - 1] depreciation_amount += flt(value_after_depreciation - diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 9d06caeb982..3328e664fc0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -28,7 +28,8 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption) + frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption); + frm.toggle_display('asset_depreciation_details_section', frm.doc.capitalize_repair_cost); if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 74d3114ea48..95abbd3e94f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -48,6 +48,7 @@ class AssetRepair(Document): if self.capitalize_repair_cost: self.check_for_purchase_invoice() self.make_gl_entries() + self.modify_depreciation_schedule() def check_repair_status(self): if self.repair_status == "Pending": @@ -140,8 +141,16 @@ class AssetRepair(Document): for account in asset_category.accounts: if account.company_name == company: return account.fixed_asset_account + + def modify_depreciation_schedule(self): + if self.increase_in_asset_life: + asset = frappe.get_doc('Asset', self.asset) + asset.flags.ignore_validate_update_after_submit = True + for row in asset.finance_books: + row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + asset.prepare_depreciation_data() + asset.save() - @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) From 4277877883c120789f3867d20b88c7eabd7726f9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 10:40:16 +0530 Subject: [PATCH 045/376] feat(Asset Repair): Change visibilty of sections --- .../doctype/asset_repair/asset_repair.js | 28 +------------------ .../doctype/asset_repair/asset_repair.json | 7 +++-- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 3328e664fc0..7633a595a2f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,35 +2,9 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { - // setup: function(frm) { - // frm.add_fetch("company", "repair_and_maintenance_account", "payable_account"); - - // frm.set_query("payable_account", function() { - // return { - // filters: { - // "report_type": "Balance Sheet", - // "account_type": "Payable", - // "company": frm.doc.company, - // "is_group": 0 - // } - // }; - // }); - // }, - - // stock_items_add: function(frm){ - // var table = frm.doc.stock_items; - // for(var i in table) { - // if (table[i].valuation_rate == 0) { - // frm.set_value(table[i].total_value, (table[i].valuation_rate * table[i].consumed_quantity)) - // } - // } - // }, - refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption); - frm.toggle_display('asset_depreciation_details_section', frm.doc.capitalize_repair_cost); - + if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { frappe.route_options = { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index a2d962cd959..522f2874d99 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -201,11 +201,13 @@ "label": "Stock Consumed During Repair" }, { + "depends_on": "stock_consumption", "fieldname": "stock_consumption_details_section", "fieldtype": "Section Break", "label": "Stock Consumption Details" }, { + "depends_on": "stock_consumption", "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost" @@ -217,6 +219,7 @@ "options": "Warehouse" }, { + "depends_on": "capitalize_repair_cost", "fieldname": "asset_depreciation_details_section", "fieldtype": "Section Break", "label": "Asset Depreciation Details" @@ -224,7 +227,7 @@ { "fieldname": "increase_in_asset_life", "fieldtype": "Int", - "label": "Increase In Asset Life" + "label": "Increase In Asset Life(Months)" }, { "fieldname": "purchase_invoice", @@ -236,7 +239,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-14 02:31:57.226273", + "modified": "2021-05-21 10:37:35.002238", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 4b543de3293d0dfa1a2cbb51501ff7a863b64805 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 23:40:36 +0530 Subject: [PATCH 046/376] feat(Asset Repair): Modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation --- erpnext/assets/doctype/asset/asset.json | 16 ++++++++++++- erpnext/assets/doctype/asset/asset.py | 7 +++--- .../doctype/asset_repair/asset_repair.py | 24 +++++++++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 4960c7e7095..8a0e3ad2a66 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -54,6 +54,8 @@ "next_depreciation_date", "section_break_14", "schedules", + "to_date", + "edit_dates", "insurance_details", "policy_number", "insurer", @@ -487,6 +489,18 @@ "fieldtype": "Currency", "label": "Asset Value", "read_only": 1 + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "hidden": 1, + "label": "To Date" + }, + { + "fieldname": "edit_dates", + "fieldtype": "Data", + "hidden": 1, + "label": "Edit Dates" } ], "idx": 72, @@ -509,7 +523,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-05-11 23:47:15.831720", + "modified": "2021-05-21 12:05:29.424083", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f837c5e7c19..2d012d672e9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -228,11 +228,12 @@ class Asset(AccountsController): # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + if not self.edit_dates: + self.to_date = add_months(self.available_for_use_date, + n * cint(d.frequency_of_depreciation)) depreciation_amount, days, months = get_pro_rata_amt(d, - depreciation_amount, schedule_date, to_date) + depreciation_amount, schedule_date, self.to_date) monthly_schedule_date = add_months(schedule_date, 1) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 95abbd3e94f..8fd019febd0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,9 +5,8 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate +from frappe.utils import time_diff_in_hours, getdate, add_days, date_diff, add_months, flt, cint from frappe.model.document import Document -from frappe.utils import flt from erpnext.accounts.general_ledger import make_gl_entries class AssetRepair(Document): @@ -148,8 +147,29 @@ class AssetRepair(Document): asset.flags.ignore_validate_update_after_submit = True for row in asset.finance_books: row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + + asset.edit_dates = "" + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date(asset, row, extra_months) + # fix depreciation amount + asset.prepare_depreciation_data() asset.save() + + # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation + def calculate_last_schedule_date(self, asset, row, extra_months): + asset.edit_dates = "Don't Edit" + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ + cint(asset.number_of_depreciations_booked) + last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + asset.to_date = add_months(last_schedule_date, extra_months) + schedule_date = add_months(row.depreciation_start_date, + number_of_pending_depreciations * cint(row.frequency_of_depreciation)) + + if asset.to_date > schedule_date: + row.total_number_of_depreciations += 1 + @frappe.whitelist() def get_downtime(failure_date, completion_date): From 28ca383534fad7734a82fe69502c1f6e1fcac72d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 23:55:51 +0530 Subject: [PATCH 047/376] feat(Asset): Edit value_after_depreciation --- erpnext/assets/doctype/asset/asset.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2d012d672e9..fb29ea05695 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -183,13 +183,12 @@ class Asset(AccountsController): start = 0 for n in range (len(self.schedules)): if not self.schedules[n].journal_entry: - print("*"*100) del self.schedules[n:] start = n break - value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = (flt(self.asset_value) - + flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) d.value_after_depreciation = value_after_depreciation From 1d7dda2664d8966601c9520efd199a99e2b35497 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 22 May 2021 07:18:31 +0530 Subject: [PATCH 048/376] fix(Asset): Fix depreciation_amount calculation --- erpnext/assets/doctype/asset/asset.py | 37 +++++++++---------- .../doctype/asset_repair/asset_repair.py | 1 - 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index fb29ea05695..e61887a1f11 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -205,8 +205,7 @@ class Asset(AccountsController): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = self.get_depreciation_amount(value_after_depreciation, - d.total_number_of_depreciations, d) + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: schedule_date = add_months(d.depreciation_start_date, @@ -377,24 +376,6 @@ class Asset(AccountsController): def get_value_after_depreciation(self, idx): return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation) - def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row): - precision = self.precision("gross_purchase_amount") - - if row.depreciation_method in ("Straight Line", "Manual"): - depreciation_left = (cint(row.total_number_of_depreciations) - cint(self.number_of_depreciations_booked)) - - if not depreciation_left: - frappe.msgprint(_("All the depreciations has been booked")) - depreciation_amount = flt(row.expected_value_after_useful_life) - return depreciation_amount - - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left - else: - depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100), precision) - - return depreciation_amount - def validate_expected_value_after_useful_life(self): for row in self.get('finance_books'): accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount @@ -791,3 +772,19 @@ def get_total_days(date, frequency): cint(frequency) * -1) return date_diff(date, period_start_date) + +@erpnext.allow_regional +def get_depreciation_amount(asset, depreciable_value, row): + depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) + + if row.depreciation_method in ("Straight Line", "Manual"): + if not asset.to_date: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / depreciation_left + else: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + else: + depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) + + return depreciation_amount diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 8fd019febd0..9973afd80ad 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -152,7 +152,6 @@ class AssetRepair(Document): extra_months = self.increase_in_asset_life % row.frequency_of_depreciation if extra_months != 0: self.calculate_last_schedule_date(asset, row, extra_months) - # fix depreciation amount asset.prepare_depreciation_data() asset.save() From 03a6977a01490c35d9ad51bb7a94dd8fd08d2b2e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 22 May 2021 23:23:29 +0530 Subject: [PATCH 049/376] fix: Sider issues --- erpnext/assets/doctype/asset/asset.js | 2 +- erpnext/assets/doctype/asset/asset.py | 2 +- erpnext/assets/doctype/asset_maintenance/asset_maintenance.js | 2 +- erpnext/assets/doctype/asset_repair/asset_repair.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 1e67ec816b6..922cc4a7b26 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -319,7 +319,7 @@ frappe.ui.form.on('Asset', { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } - }) + }); }, create_asset_adjustment: function(frm) { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e61887a1f11..1495a5fa5f4 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -181,7 +181,7 @@ class Asset(AccountsController): self.validate_asset_finance_books(d) start = 0 - for n in range (len(self.schedules)): + for n in range(len(self.schedules)): if not self.schedules[n].journal_entry: del self.schedules[n:] start = n diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 3830d1168c4..19393b7e9db 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -31,7 +31,7 @@ frappe.ui.form.on('Asset Maintenance', { frm.trigger('make_dashboard'); } - frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) + frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption); }, make_dashboard: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9973afd80ad..3e81ba55b00 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, add_days, date_diff, add_months, flt, cint +from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries From 6432aaa07abc3abcb339bf73a096ce93b78269d0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 12 May 2021 16:34:09 +0530 Subject: [PATCH 050/376] feat: added reports to check incorrect qty and valuation for serialized items --- .../__init__.py | 0 ...incorrect_balance_qty_after_transaction.js | 27 ++++ ...correct_balance_qty_after_transaction.json | 32 ++++ ...incorrect_balance_qty_after_transaction.py | 111 +++++++++++++ .../incorrect_serial_no_valuation/__init__.py | 0 .../incorrect_serial_no_valuation.js | 35 +++++ .../incorrect_serial_no_valuation.json | 36 +++++ .../incorrect_serial_no_valuation.py | 148 ++++++++++++++++++ erpnext/stock/workspace/stock/stock.json | 38 ++++- 9 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/__init__.py create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js new file mode 100644 index 00000000000..bf11277d9c4 --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js @@ -0,0 +1,27 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Balance Qty After Transaction"] = { + "filters": [ + { + label: __("Company"), + fieldtype: "Link", + fieldname: "company", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __('Item Code'), + fieldtype: 'Link', + fieldname: 'item_code', + options: 'Item' + }, + { + label: __('Warehouse'), + fieldtype: 'Link', + fieldname: 'warehouse' + } + ] +}; diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json new file mode 100644 index 00000000000..a5815bcca49 --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-05-12 16:47:58.717853", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-05-12 16:48:28.347575", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Balance Qty After Transaction", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Balance Qty After Transaction", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + }, + { + "role": "Purchase User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py new file mode 100644 index 00000000000..cf174c93682 --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py @@ -0,0 +1,111 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from six import iteritems +from frappe.utils import flt + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters): + data = get_stock_ledger_entries(filters) + itewise_balance_qty = {} + + for row in data: + key = (row.item_code, row.warehouse) + itewise_balance_qty.setdefault(key, []).append(row) + + res = validate_data(itewise_balance_qty) + return res + +def validate_data(itewise_balance_qty): + res = [] + for key, data in iteritems(itewise_balance_qty): + row = get_incorrect_data(data) + if row: + res.append(row) + res.append({}) + + return res + +def get_incorrect_data(data): + balance_qty = 0.0 + for row in data: + balance_qty += row.actual_qty + if row.voucher_type == "Stock Reconciliation" and not row.batch_no: + balance_qty = flt(row.qty_after_transaction) + + row.expected_balance_qty = balance_qty + if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5: + row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) + return row + +def get_stock_ledger_entries(report_filters): + filters = {} + fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty', + 'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no'] + + for field in ['warehouse', 'item_code', 'company']: + if report_filters.get(field): + filters[field] = report_filters.get(field) + + return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, + order_by = 'timestamp(posting_date, posting_time) asc, creation asc') + +def get_columns(): + return [{ + 'label': _('Id'), + 'fieldtype': 'Link', + 'fieldname': 'name', + 'options': 'Stock Ledger Entry', + 'width': 120 + }, { + 'label': _('Posting Date'), + 'fieldtype': 'Date', + 'fieldname': 'posting_date', + 'width': 110 + }, { + 'label': _('Voucher Type'), + 'fieldtype': 'Link', + 'fieldname': 'voucher_type', + 'options': 'DocType', + 'width': 120 + }, { + 'label': _('Voucher No'), + 'fieldtype': 'Dynamic Link', + 'fieldname': 'voucher_no', + 'options': 'voucher_type', + 'width': 120 + }, { + 'label': _('Item Code'), + 'fieldtype': 'Link', + 'fieldname': 'item_code', + 'options': 'Item', + 'width': 120 + }, { + 'label': _('Warehouse'), + 'fieldtype': 'Link', + 'fieldname': 'warehouse', + 'options': 'Warehouse', + 'width': 120 + }, { + 'label': _('Expected Balance Qty'), + 'fieldtype': 'Float', + 'fieldname': 'expected_balance_qty', + 'width': 170 + }, { + 'label': _('Actual Balance Qty'), + 'fieldtype': 'Float', + 'fieldname': 'qty_after_transaction', + 'width': 150 + }, { + 'label': _('Difference'), + 'fieldtype': 'Float', + 'fieldname': 'differnce', + 'width': 110 + }] \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js new file mode 100644 index 00000000000..c62d48081c2 --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js @@ -0,0 +1,35 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Serial No Valuation"] = { + "filters": [ + { + label: __('Item Code'), + fieldtype: 'Link', + fieldname: 'item_code', + options: 'Item', + get_query: function() { + return { + filters: { + 'has_serial_no': 1 + } + } + } + }, + { + label: __('From Date'), + fieldtype: 'Date', + fieldname: 'from_date', + reqd: 1, + default: frappe.defaults.get_user_default("year_start_date") + }, + { + label: __('To Date'), + fieldtype: 'Date', + fieldname: 'to_date', + reqd: 1, + default: frappe.defaults.get_user_default("year_end_date") + } + ] +}; diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json new file mode 100644 index 00000000000..cc384a5bd03 --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json @@ -0,0 +1,36 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-05-13 13:07:00.767845", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2021-05-13 13:07:00.767845", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Serial No Valuation", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Serial No Valuation", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py new file mode 100644 index 00000000000..e54cf4c66c7 --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -0,0 +1,148 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +import copy +from frappe import _ +from six import iteritems +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters): + data = get_stock_ledger_entries(filters) + serial_nos_data = prepare_serial_nos(data) + data = get_incorrect_serial_nos(serial_nos_data) + + return data + +def prepare_serial_nos(data): + serial_no_wise_data = {} + for row in data: + if not row.serial_nos: + continue + + for serial_no in get_serial_nos(row.serial_nos): + sle = copy.deepcopy(row) + sle.serial_no = serial_no + sle.qty = 1 if sle.actual_qty > 0 else -1 + sle.valuation_rate = sle.valuation_rate if sle.actual_qty > 0 else sle.valuation_rate * -1 + serial_no_wise_data.setdefault(serial_no, []).append(sle) + + return serial_no_wise_data + +def get_incorrect_serial_nos(serial_nos_data): + result = [] + + total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))}) + + for serial_no, data in iteritems(serial_nos_data): + total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))}) + + if check_incorrect_serial_data(data, total_dict): + result.extend(data) + + total_value.qty += total_dict.qty + total_value.valuation_rate += total_dict.valuation_rate + + result.append(total_dict) + result.append({}) + + result.append(total_value) + + return result + +def check_incorrect_serial_data(data, total_dict): + incorrect_data = False + for row in data: + total_dict.qty += row.qty + total_dict.valuation_rate += row.valuation_rate + + if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0): + incorrect_data = True + + return incorrect_data + +def get_stock_ledger_entries(report_filters): + fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty', + 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate'] + + filters = {'serial_no': ("is", "set")} + + if report_filters.get('item_code'): + filters['item_code'] = report_filters.get('item_code') + + if report_filters.get('from_date') and report_filters.get('to_date'): + filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')]) + + return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, + order_by = 'timestamp(posting_date, posting_time) asc, creation asc') + +def get_columns(): + return [{ + 'label': _('Company'), + 'fieldtype': 'Link', + 'fieldname': 'company', + 'options': 'Company', + 'width': 120 + }, { + 'label': _('Id'), + 'fieldtype': 'Link', + 'fieldname': 'name', + 'options': 'Stock Ledger Entry', + 'width': 120 + }, { + 'label': _('Posting Date'), + 'fieldtype': 'Date', + 'fieldname': 'posting_date', + 'width': 90 + }, { + 'label': _('Posting Time'), + 'fieldtype': 'Time', + 'fieldname': 'posting_time', + 'width': 90 + }, { + 'label': _('Voucher Type'), + 'fieldtype': 'Link', + 'fieldname': 'voucher_type', + 'options': 'DocType', + 'width': 100 + }, { + 'label': _('Voucher No'), + 'fieldtype': 'Dynamic Link', + 'fieldname': 'voucher_no', + 'options': 'voucher_type', + 'width': 110 + }, { + 'label': _('Item Code'), + 'fieldtype': 'Link', + 'fieldname': 'item_code', + 'options': 'Item', + 'width': 120 + }, { + 'label': _('Warehouse'), + 'fieldtype': 'Link', + 'fieldname': 'warehouse', + 'options': 'Warehouse', + 'width': 120 + }, { + 'label': _('Serial No'), + 'fieldtype': 'Link', + 'fieldname': 'serial_no', + 'options': 'Serial No', + 'width': 100 + }, { + 'label': _('Qty'), + 'fieldtype': 'Float', + 'fieldname': 'qty', + 'width': 80 + }, { + 'label': _('Valuation Rate (In / Out)'), + 'fieldtype': 'Currency', + 'fieldname': 'valuation_rate', + 'width': 110 + }] \ No newline at end of file diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index 3221dc4365c..529ce8eb61e 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -15,6 +15,7 @@ "hide_custom": 0, "icon": "stock", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Stock", "links": [ @@ -653,9 +654,44 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Incorrect Data Report", + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Incorrect Serial No Qty and Valuation", + "link_to": "Incorrect Serial No Valuation", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Incorrect Balance Qty After Transaction", + "link_to": "Incorrect Balance Qty After Transaction", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Stock and Account Value Comparison", + "link_to": "Stock and Account Value Comparison", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], - "modified": "2020-12-01 13:38:36.282890", + "modified": "2021-05-13 13:10:24.914983", "modified_by": "Administrator", "module": "Stock", "name": "Stock", From c6dcc9d94a19fef68ebaf792063beb0fd19c8c3a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Jun 2021 13:13:04 +0530 Subject: [PATCH 051/376] fix(India): Taxable value for invoices with additional discount --- erpnext/regional/india/e_invoice/utils.py | 30 ++++++----------------- erpnext/regional/india/utils.py | 8 ++---- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 843fb012b91..95b4e16afda 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -38,7 +38,7 @@ def validate_eligibility(doc): einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): return False - + invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') @@ -135,7 +135,7 @@ def validate_address_fields(address, is_shipping_address): def get_party_details(address_name, is_shipping_address=False): addr = frappe.get_doc('Address', address_name) - + validate_address_fields(addr, is_shipping_address) if addr.gst_state_number == 97: @@ -188,11 +188,6 @@ def get_item_list(invoice): item.qty = abs(item.qty) - if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - item.discount_amount = abs(item.base_amount - item.base_net_amount) - else: - item.discount_amount = 0 - item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) item.gross_amount = abs(item.taxable_value) + item.discount_amount item.taxable_value = abs(item.taxable_value) @@ -254,18 +249,8 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - - if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - # Discount already applied on net total which means on items - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - invoice_value_details.invoice_discount_amt = 0 - elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount: - invoice_value_details.invoice_discount_amt = invoice.base_discount_amount - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - else: - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - # since tax already considers discount amount - invoice_value_details.invoice_discount_amt = 0 + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.invoice_discount_amt = 0 invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) @@ -287,8 +272,7 @@ def update_invoice_taxes(invoice, invoice_value_details): considered_rows = [] for t in invoice.taxes: - tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \ - else t.base_tax_amount_after_discount_amount + tax_amount = t.base_tax_amount_after_discount_amount if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: # using after discount amt since item also uses after discount amt for cess calc @@ -995,7 +979,7 @@ class GSPConnector(): self.invoice.failure_description = self.get_failure_message(errors) if errors else "" self.update_invoice() frappe.db.commit() - + def get_failure_message(self, errors): if isinstance(errors, list): errors = ', '.join(errors) @@ -1052,7 +1036,7 @@ def generate_einvoices(docnames): _('{} e-invoices generated successfully').format(success), title=_('Bulk E-Invoice Generation Complete') ) - + else: enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fc227defbfc..ea615020998 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -817,12 +817,8 @@ def update_taxable_values(doc, method): considered_rows.append(prev_row_id) for item in doc.get('items'): - if doc.apply_discount_on == 'Grand Total' and doc.discount_amount: - proportionate_value = item.base_amount if doc.base_total else item.qty - total_value = doc.base_total if doc.base_total else doc.total_qty - else: - proportionate_value = item.base_net_amount if doc.base_net_total else item.qty - total_value = doc.base_net_total if doc.base_net_total else doc.total_qty + proportionate_value = item.base_net_amount if doc.base_net_total else item.qty + total_value = doc.base_net_total if doc.base_net_total else doc.total_qty applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), item.precision('taxable_value'))) From 960840dc3ad9c4f12268dc06f3f00fcc7fb4ace8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 01:53:13 +0530 Subject: [PATCH 052/376] fix(Asset Repair): Set completion_date --- .../assets/doctype/asset_repair/asset_repair.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3e81ba55b00..ad0792e3ea7 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,18 +5,19 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint +from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, cint from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries class AssetRepair(Document): def validate(self): if self.repair_status == "Completed" and not self.completion_date: - frappe.throw(_("Please select Completion Date for Completed Repair")) + self.completion_date = nowdate() self.update_status() - self.set_total_value() # change later - self.calculate_total_repair_cost() + if self.stock_consumption: + self.set_total_value() # change later + self.calculate_total_repair_cost() def update_status(self): if self.repair_status == 'Pending': @@ -31,9 +32,8 @@ class AssetRepair(Document): def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost - if self.stock_consumption: - for item in self.stock_items: - self.total_repair_cost += item.total_value + for item in self.stock_items: + self.total_repair_cost += item.total_value def on_submit(self): self.check_repair_status() From 2ceeb8138d03076e7192b9a97bd14741976ce5fc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 01:54:44 +0530 Subject: [PATCH 053/376] fix(Asset Repair): Set company when creating Stock Entry --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ad0792e3ea7..c39b20c50de 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -75,7 +75,8 @@ class AssetRepair(Document): def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", - "stock_entry_type": "Material Issue" + "stock_entry_type": "Material Issue", + "company": frappe.get_value('Asset', self.asset, "company") }) for stock_item in self.stock_items: From 9fb47795c4b3d03ee36273959b4b844dacb35bb7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 04:55:49 +0530 Subject: [PATCH 054/376] fix(Asset Repair): Only modify depreciation schedule if calculate_depreciation is checked --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index c39b20c50de..fa918b7efd2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -47,7 +47,8 @@ class AssetRepair(Document): if self.capitalize_repair_cost: self.check_for_purchase_invoice() self.make_gl_entries() - self.modify_depreciation_schedule() + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation'): + self.modify_depreciation_schedule() def check_repair_status(self): if self.repair_status == "Pending": From 034f7bde336a8bfa69787d382634c7848511584d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 21:07:07 +0530 Subject: [PATCH 055/376] fix(Asset Repair): Remove unnecessary condition --- erpnext/assets/doctype/asset_repair/asset_repair.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index fa918b7efd2..80c9f09e145 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -15,9 +15,8 @@ class AssetRepair(Document): self.completion_date = nowdate() self.update_status() - if self.stock_consumption: - self.set_total_value() # change later - self.calculate_total_repair_cost() + self.set_total_value() # change later + self.calculate_total_repair_cost() def update_status(self): if self.repair_status == 'Pending': @@ -72,7 +71,7 @@ class AssetRepair(Document): if self.capitalize_repair_cost: asset_value += self.repair_cost frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) - + def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", From aa9bbe51bd0205918f75cc035efb9c07b055ca80 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 21:28:58 +0530 Subject: [PATCH 056/376] fix(Asset Repair): Add Company in GL Entries --- erpnext/assets/doctype/asset_repair/asset_repair.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 80c9f09e145..0b4612e7546 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -117,7 +117,8 @@ class AssetRepair(Document): "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate() + "posting_date": getdate(), + "company": company }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -131,7 +132,8 @@ class AssetRepair(Document): "cost_center": self.cost_center, "posting_date": getdate(), "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice + "against_voucher": self.purchase_invoice, + "company": company }) gl_entry.insert() From b55649f2ecf1b4b0f4121fe0d7ad986517d021b8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 22:28:30 +0530 Subject: [PATCH 057/376] fix(Asset): Add depreciation schedule details in create_asset() --- erpnext/assets/doctype/asset/test_asset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a0d76031fc4..568a41039cb 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -660,7 +660,7 @@ def create_asset(**args): "item_code": args.item_code or "Macbook Pro", "company": args.company or"_Test Company", "purchase_date": "2015-01-01", - "calculate_depreciation": 0, + "calculate_depreciation": args.calculate_depreciation or 0, "gross_purchase_amount": 100000, "purchase_receipt_amount": 100000, "expected_value_after_useful_life": 10000, @@ -671,6 +671,13 @@ def create_asset(**args): "is_existing_asset": args.is_existing_asset or 0 }) + if asset.calculate_depreciation: + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 5 + }) + try: asset.save() except frappe.DuplicateEntryError: From 12d9e3b1e6e04dbc2eae8f2f7a588fbafbdfe59b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 22:28:57 +0530 Subject: [PATCH 058/376] fix(Asset Repair): Add tests --- .../doctype/asset_repair/asset_repair.py | 1 - .../doctype/asset_repair/test_asset_repair.py | 166 +++++++++++++++++- 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 0b4612e7546..8724d0a1254 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -172,7 +172,6 @@ class AssetRepair(Document): if asset.to_date > schedule_date: row.total_number_of_depreciations += 1 - @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 3d325a9683c..9c9dd449713 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -2,8 +2,170 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - +import frappe +from frappe.utils import nowdate, flt import unittest +from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company class TestAssetRepair(unittest.TestCase): - pass + def setUp(self): + set_depreciation_settings_in_company() + create_asset_data() + frappe.db.sql("delete from `tabTax Rule`") + + def test_completion_date(self): + asset_repair = create_asset_repair() + asset_repair.repair_status = "Completed" + asset_repair.save() + self.assertTrue(asset_repair.completion_date) + + def test_update_status(self): + asset = create_asset() + initial_status = asset.status + asset_repair = create_asset_repair(asset = asset) + + if asset_repair.repair_status == "Pending": + asset.reload() + self.assertEqual(asset.status, "Out of Order") + + asset_repair.repair_status = "Completed" + asset_repair.save() + asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status") + self.assertEqual(asset_status, initial_status) + + def test_stock_item_total_value(self): + asset_repair = create_asset_repair(stock_consumption = 1) + + for item in asset_repair.stock_items: + total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + self.assertEqual(item.total_value, total_value) + + def test_total_repair_cost(self): + asset_repair = create_asset_repair(stock_consumption = 1) + + total_repair_cost = asset_repair.repair_cost + self.assertEqual(total_repair_cost, asset_repair.repair_cost) + for item in asset_repair.stock_items: + total_repair_cost += item.total_value + + self.assertEqual(total_repair_cost, asset_repair.total_repair_cost) + + def test_repair_status_after_submit(self): + asset_repair = create_asset_repair(submit = 1) + self.assertNotEqual(asset_repair.repair_status, "Pending") + + def test_stock_items(self): + asset_repair = create_asset_repair(stock_consumption = 1) + self.assertTrue(asset_repair.stock_consumption) + self.assertTrue(asset_repair.stock_items) + + def test_warehouse(self): + asset_repair = create_asset_repair(stock_consumption = 1) + self.assertTrue(asset_repair.stock_consumption) + self.assertTrue(asset_repair.warehouse) + + def test_decrease_stock_quantity(self): + asset_repair = create_asset_repair(stock_consumption = 1, submit = 1) + stock_entry = frappe.get_last_doc('Stock Entry') + + self.assertEqual(stock_entry.stock_entry_type, "Material Issue") + self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) + self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) + self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) + + def test_increase_in_asset_value_due_to_stock_consumption(self): + asset = create_asset() + initial_asset_value = asset.asset_value + asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1) + asset.reload() + + increase_in_asset_value = asset.asset_value - initial_asset_value + self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) + + def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): + asset = create_asset() + initial_asset_value = asset.asset_value + asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + asset.reload() + + increase_in_asset_value = asset.asset_value - initial_asset_value + self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) + + def test_purchase_invoice(self): + asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) + self.assertTrue(asset_repair.purchase_invoice) + + def test_gl_entries(self): + asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) + gl_entry = frappe.get_last_doc('GL Entry') + self.assertEqual(asset_repair.name, gl_entry.voucher_no) + + def test_increase_in_asset_life(self): + asset = create_asset(calculate_depreciation = 1) + initial_num_of_depreciations = num_of_depreciations(asset) + create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + asset.reload() + self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) + +def num_of_depreciations(asset): + return asset.finance_books[0].total_number_of_depreciations + +def create_asset_repair(**args): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + + args = frappe._dict(args) + + if args.asset: + asset = args.asset + else: + asset = create_asset(is_existing_asset = 1) + asset_repair = frappe.new_doc("Asset Repair") + asset_repair.update({ + "asset": asset.name, + "asset_name": asset.asset_name, + "failure_date": nowdate(), + "description": "Test Description", + "repair_cost": 0 + }) + + if args.stock_consumption: + asset_repair.stock_consumption = 1 + asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) + asset_repair.append("stock_items", { + "item": args.item or args.item_code or "_Test Item", + "valuation_rate": args.rate if args.get("rate") is not None else 100, + "consumed_quantity": args.qty or 1 + }) + + try: + asset_repair.save() + except frappe.DuplicateEntryError: + pass + + if args.submit: + asset_repair.repair_status = "Completed" + asset_repair.cost_center = "_Test Cost Center - _TC" + + if args.stock_consumption: + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Receipt", + "company": asset.company + }) + stock_entry.append('items', { + "t_warehouse": asset_repair.warehouse, + "item_code": asset_repair.stock_items[0].item, + "qty": asset_repair.stock_items[0].consumed_quantity + }) + stock_entry.submit() + + if args.capitalize_repair_cost: + asset_repair.capitalize_repair_cost = 1 + asset_repair.repair_cost = 1000 + if asset.calculate_depreciation: + asset_repair.increase_in_asset_life = 12 + asset_repair.purchase_invoice = make_purchase_invoice().name + + asset_repair.submit() + return asset_repair \ No newline at end of file From 48036b4bab25fcecf2728decc78ae2c598f54c0e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 4 Jun 2021 09:54:34 +0530 Subject: [PATCH 059/376] fix: invoices can alter profit and loss of a closed year --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 948c51364ed..11465b711e3 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -121,8 +121,7 @@ class GLEntry(Document): def check_pl_account(self): if self.is_opening=='Yes' and \ - frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \ - self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']: + frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss": frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") .format(self.voucher_type, self.voucher_no, self.account)) From edecd5b0c686f653454fa3d85fa87e058ad81f9b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Jun 2021 12:04:30 +0530 Subject: [PATCH 060/376] fix: Update einvoice json test --- .../sales_invoice/test_sales_invoice.py | 98 ++++++++----------- 1 file changed, 41 insertions(+), 57 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index df6d4839041..b35686f4f0b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1899,69 +1899,53 @@ class TestSalesInvoice(unittest.TestCase): frappe.flags.country = country def test_einvoice_json(self): - from erpnext.regional.india.e_invoice.utils import make_einvoice + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals - si = make_sales_invoice_for_ewaybill() - si.naming_series = 'INV-2020-.#####' - si.items = [] - si.append("items", { - "item_code": "_Test Item", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 2000, - "rate": 12, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) - si.append("items", { - "item_code": "_Test Item 2", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 420, - "rate": 15, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) + si = get_sales_invoice_for_e_invoice() si.discount_amount = 100 si.save() einvoice = make_einvoice(si) - - total_item_ass_value = 0 - total_item_cgst_value = 0 - total_item_sgst_value = 0 - total_item_igst_value = 0 - total_item_value = 0 - - for item in einvoice['ItemList']: - total_item_ass_value += item['AssAmt'] - total_item_cgst_value += item['CgstAmt'] - total_item_sgst_value += item['SgstAmt'] - total_item_igst_value += item['IgstAmt'] - total_item_value += item['TotItemVal'] - - self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount']) - self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt']) - - value_details = einvoice['ValDtls'] - - self.assertEqual(einvoice['Version'], '1.1') - self.assertEqual(value_details['AssVal'], total_item_ass_value) - self.assertEqual(value_details['CgstVal'], total_item_cgst_value) - self.assertEqual(value_details['SgstVal'], total_item_sgst_value) - self.assertEqual(value_details['IgstVal'], total_item_igst_value) - - calculated_invoice_value = \ - value_details['AssVal'] + value_details['CgstVal'] \ - + value_details['SgstVal'] + value_details['IgstVal'] \ - + value_details['OthChrg'] - value_details['Discount'] - - self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1) - - self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) + validate_totals(einvoice) + + si.apply_discount_on = 'Net Total' + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + + [d.set('included_in_print_rate', 1) for d in si.taxes] + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + +def get_sales_invoice_for_e_invoice(): + si = make_sales_invoice_for_ewaybill() + si.naming_series = 'INV-2020-.#####' + si.items = [] + si.append("items", { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2000, + "rate": 12, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + + si.append("items", { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 420, + "rate": 15, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + + return si def make_test_address_for_ewaybill(): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): From 88176e65e46c43f1a5a6e88f3d08a2bcb6e3e5fb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Jun 2021 17:31:28 +0530 Subject: [PATCH 061/376] fix: Check for gst_hsn_code --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5a53d493994..11ebef724c4 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -194,7 +194,7 @@ def get_item_list(invoice): item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None - item.is_service_item = 'Y' if item.gst_hsn_code[:2] == "99" else 'N' + item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N' item.serial_no = "" item = update_item_taxes(invoice, item) From 687ad9b9421074a073d3087e3c266a72473bb7c7 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Jun 2021 21:18:19 +0530 Subject: [PATCH 062/376] fix: Report Raw Materials to be Transferred --- ...tracted_raw_materials_to_be_transferred.py | 118 ++++++------------ 1 file changed, 39 insertions(+), 79 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index de2ae8fc73d..5a0381b0171 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -9,10 +9,10 @@ def execute(filters=None): if filters.from_date >= filters.to_date: frappe.msgprint(_("To Date must be greater than From Date")) - data = [] columns = get_columns() - get_data(data , filters) - return columns, data + data = get_data(filters) + + return columns, data or [] def get_columns(): return [ @@ -21,13 +21,12 @@ def get_columns(): "fieldtype": "Link", "fieldname": "purchase_order", "options": "Purchase Order", - "width": 150 + "width": 200 }, { "label": _("Date"), "fieldtype": "Date", "fieldname": "date", - "hidden": 1, "width": 150 }, { @@ -41,97 +40,58 @@ def get_columns(): "label": _("Item Code"), "fieldtype": "Data", "fieldname": "rm_item_code", - "width": 100 + "width": 150 }, { "label": _("Required Quantity"), "fieldtype": "Float", - "fieldname": "r_qty", - "width": 100 + "fieldname": "reqd_qty", + "width": 150 }, { "label": _("Transferred Quantity"), "fieldtype": "Float", - "fieldname": "t_qty", - "width": 100 + "fieldname": "transferred_qty", + "width": 200 }, { "label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", - "width": 100 + "width": 150 } ] -def get_data(data, filters): - po = get_po(filters) - po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po])) +def get_data(filters): + po_rm_item_details = get_po_items_to_supply(filters) - sub_items = get_purchase_order_item_supplied([v.name for v in po]) + data = [] + for row in po_rm_item_details: + transferred_qty = row.get("transferred_qty") or 0 + if transferred_qty < row.get("reqd_qty", 0): + pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) + row.p_qty = pending_qty if pending_qty > 0 else 0 + data.append(row) - for order in po: - for item in sub_items: - if order.name == item.parent and order.name in po_transferred_qty_map and \ - item.required_qty != po_transferred_qty_map.get(order.name).get(item.rm_item_code): - transferred_qty = po_transferred_qty_map.get(order.name).get(item.rm_item_code) \ - if po_transferred_qty_map.get(order.name).get(item.rm_item_code) else 0 - row ={ - 'purchase_order': item.parent, - 'date': order.transaction_date, - 'supplier': order.supplier, - 'rm_item_code': item.rm_item_code, - 'r_qty': item.required_qty, - 't_qty':transferred_qty, - 'p_qty':item.required_qty - transferred_qty - } + return data - data.append(row) - - return(data) - -def get_po(filters): - record_filters = [ - ["is_subcontracted", "=", "Yes"], - ["supplier", "=", filters.supplier], - ["transaction_date", "<=", filters.to_date], - ["transaction_date", ">=", filters.from_date], - ["docstatus", "=", 1] - ] - return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]) - -def get_transferred_quantity(po_name): - stock_entries = get_stock_entry(po_name) - stock_entries_detail = get_stock_entry_detail([v.name for v in stock_entries]) - po_transferred_qty_map = {} - - - for entry in stock_entries: - for details in stock_entries_detail: - if details.parent == entry.name: - details["Purchase_order"] = entry.purchase_order - if entry.purchase_order not in po_transferred_qty_map: - po_transferred_qty_map[entry.purchase_order] = {} - po_transferred_qty_map[entry.purchase_order][details.item_code] = details.qty - else: - po_transferred_qty_map[entry.purchase_order][details.item_code] = po_transferred_qty_map[entry.purchase_order].get(details.item_code, 0) + details.qty - - return po_transferred_qty_map - - -def get_stock_entry(po): - return frappe.get_all("Stock Entry", filters=[ - ('purchase_order', 'IN', po), - ('stock_entry_type', '=', 'Send to Subcontractor'), - ('docstatus', '=', 1) - ], fields=["name", "purchase_order"]) - -def get_stock_entry_detail(se): - return frappe.get_all("Stock Entry Detail", filters=[ - ["parent", "in", se] +def get_po_items_to_supply(filters): + return frappe.db.get_all( + "Purchase Order", + fields=[ + "name as purchase_order", + "transaction_date as date", + "supplier as supplier", + "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code", + "`tabPurchase Order Item Supplied`.required_qty as reqd_qty", + "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty" ], - fields=["parent", "item_code", "qty"]) - -def get_purchase_order_item_supplied(po): - return frappe.get_all("Purchase Order Item Supplied", filters=[ - ('parent', 'IN', po) - ], fields=['parent', 'rm_item_code', 'required_qty']) + filters = [ + ["Purchase Order", "per_received", "<", "100"], + ["Purchase Order", "is_subcontracted", "=", "Yes"], + ["Purchase Order", "supplier", "=", filters.supplier], + ["Purchase Order", "transaction_date", "<=", filters.to_date], + ["Purchase Order", "transaction_date", ">=", filters.from_date], + ["Purchase Order", "docstatus", "=", 1] + ] + ) \ No newline at end of file From ec4a3723cc1343e1ce99d9114ecb9bd610f8c034 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Jun 2021 12:47:06 +0530 Subject: [PATCH 063/376] fix: Sider --- .../subcontracted_raw_materials_to_be_transferred.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index 5a0381b0171..68426abbb04 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -70,7 +70,7 @@ def get_data(filters): transferred_qty = row.get("transferred_qty") or 0 if transferred_qty < row.get("reqd_qty", 0): pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) - row.p_qty = pending_qty if pending_qty > 0 else 0 + row.p_qty = pending_qty if pending_qty > 0 else 0 data.append(row) return data From 5bb89b0ead96f0bb7e2346fb01bd8710bbb7a3b2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Jun 2021 15:57:01 +0530 Subject: [PATCH 064/376] test(perf): eliminate repeat creation of HSN codes (#25947) --- erpnext/regional/india/setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 229e0c031e3..3e0b9b733b6 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -27,6 +27,9 @@ def setup_company_independent_fixtures(patch=False): add_print_formats() def add_hsn_sac_codes(): + if frappe.flags.in_test and frappe.flags.created_hsn_codes: + return + # HSN codes with open(os.path.join(os.path.dirname(__file__), 'hsn_code_data.json'), 'r') as f: hsn_codes = json.loads(f.read()) @@ -38,6 +41,9 @@ def add_hsn_sac_codes(): sac_codes = json.loads(f.read()) create_hsn_codes(sac_codes, code_field="sac_code") + if frappe.flags.in_test: + frappe.flags.created_hsn_codes = True + def create_hsn_codes(data, code_field): for d in data: hsn_code = frappe.new_doc('GST HSN Code') From a9c84f749a64f2627885d61d571bf10c04fd61a8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Jun 2021 16:00:48 +0530 Subject: [PATCH 065/376] perf(minor): remove unnecessary comprehensions (#25645) --- erpnext/accounts/doctype/c_form/c_form.py | 2 +- .../doctype/coupon_code/coupon_code.py | 2 +- .../invoice_discounting/invoice_discounting.py | 2 +- .../doctype/journal_entry/journal_entry.py | 4 ++-- .../monthly_distribution.py | 2 +- .../doctype/payment_entry/payment_entry.py | 10 +++++----- .../doctype/payment_request/payment_request.py | 2 +- erpnext/accounts/doctype/pricing_rule/utils.py | 18 +++++++++--------- .../purchase_invoice/test_purchase_invoice.py | 2 +- .../test_tax_withholding_category.py | 2 +- erpnext/accounts/general_ledger.py | 2 +- .../report/cash_flow/custom_cash_flow.py | 8 ++++---- .../customer_ledger_summary.py | 2 +- .../accounts/report/financial_statements.py | 2 +- .../item_wise_purchase_register.py | 2 +- .../item_wise_sales_register.py | 2 +- .../report/pos_register/pos_register.py | 6 +++--- .../purchase_register/purchase_register.py | 14 +++++++------- .../sales_payment_summary.py | 4 ++-- .../report/sales_register/sales_register.py | 16 ++++++++-------- .../tds_computation_summary.py | 4 ++-- erpnext/accounts/report/utils.py | 2 +- erpnext/accounts/utils.py | 2 +- erpnext/assets/doctype/asset/test_asset.py | 2 +- .../asset_value_adjustment.py | 2 +- .../doctype/purchase_order/purchase_order.py | 6 +++--- .../purchase_order/test_purchase_order.py | 2 +- .../request_for_quotation.py | 2 +- erpnext/controllers/accounts_controller.py | 8 ++++---- erpnext/controllers/buying_controller.py | 6 +++--- erpnext/controllers/queries.py | 2 +- erpnext/controllers/selling_controller.py | 2 +- erpnext/controllers/status_updater.py | 4 ++-- erpnext/controllers/stock_controller.py | 6 +++--- erpnext/controllers/taxes_and_totals.py | 12 ++++++------ erpnext/controllers/tests/test_mapper.py | 4 ++-- .../controllers/website_list_for_contact.py | 2 +- .../manufacturing/doctype/job_card/job_card.py | 2 +- erpnext/projects/doctype/task/task.py | 2 +- .../projects/doctype/timesheet/timesheet.py | 4 ++-- .../report/project_summary/project_summary.py | 2 +- erpnext/selling/doctype/customer/customer.py | 2 +- erpnext/selling/doctype/quotation/quotation.py | 2 +- .../selling/doctype/sales_order/sales_order.py | 2 +- erpnext/setup/doctype/company/company.py | 4 ++-- erpnext/setup/doctype/item_group/item_group.py | 4 ++-- erpnext/stock/doctype/batch/batch.py | 2 +- .../doctype/delivery_note/delivery_note.py | 2 +- .../doctype/delivery_trip/delivery_trip.py | 6 +++--- .../landed_cost_voucher/landed_cost_voucher.py | 8 ++++---- .../test_landed_cost_voucher.py | 2 +- .../stock/doctype/packing_slip/packing_slip.py | 4 ++-- .../purchase_receipt/test_purchase_receipt.py | 2 +- erpnext/stock/doctype/serial_no/serial_no.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../item_price_stock/item_price_stock.py | 2 +- .../itemwise_recommended_reorder_level.py | 2 +- .../product_bundle_balance.py | 2 +- .../report/stock_balance/stock_balance.py | 6 +++--- .../stock/report/stock_ledger/stock_ledger.py | 4 ++-- erpnext/stock/stock_ledger.py | 2 +- .../doctype/warranty_claim/warranty_claim.py | 2 +- erpnext/utilities/product.py | 2 +- erpnext/utilities/transaction_base.py | 4 ++-- 64 files changed, 126 insertions(+), 126 deletions(-) diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py index fd86ed4c90e..cfe28f3ff9f 100644 --- a/erpnext/accounts/doctype/c_form/c_form.py +++ b/erpnext/accounts/doctype/c_form/c_form.py @@ -54,7 +54,7 @@ class CForm(Document): frappe.throw(_("Please enter atleast 1 invoice in the table")) def set_total_invoiced_amount(self): - total = sum([flt(d.grand_total) for d in self.get('invoices')]) + total = sum(flt(d.grand_total) for d in self.get('invoices')) frappe.db.set(self, 'total_invoiced_amount', total) @frappe.whitelist() diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.py b/erpnext/accounts/doctype/coupon_code/coupon_code.py index 7829c9320d7..55c119315e0 100644 --- a/erpnext/accounts/doctype/coupon_code/coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/coupon_code.py @@ -14,7 +14,7 @@ class CouponCode(Document): if not self.coupon_code: if self.coupon_type == "Promotional": - self.coupon_code =''.join([i for i in self.coupon_name if not i.isdigit()])[0:8].upper() + self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper() elif self.coupon_type == "Gift Card": self.coupon_code = frappe.generate_hash()[:10].upper() diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 95d2ee56d93..b73d8bfbb11 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -42,7 +42,7 @@ class InvoiceDiscounting(AccountsController): record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice))) def calculate_total_amount(self): - self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices]) + self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices) def on_submit(self): self.update_sales_invoice() diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ed1bd282235..937597bc550 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -196,8 +196,8 @@ class JournalEntry(AccountsController): frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account)) def check_credit_limit(self): - customers = list(set([d.party for d in self.get("accounts") - if d.party_type=="Customer" and d.party and flt(d.debit) > 0])) + customers = list(set(d.party for d in self.get("accounts") + if d.party_type=="Customer" and d.party and flt(d.debit) > 0)) if customers: from erpnext.selling.doctype.customer.customer import check_credit_limit for customer in customers: diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py index 88667d72076..bff64227325 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py @@ -21,7 +21,7 @@ class MonthlyDistribution(Document): idx += 1 def validate(self): - total = sum([flt(d.percentage_allocation) for d in self.get("percentages")]) + total = sum(flt(d.percentage_allocation) for d in self.get("percentages")) if flt(total, 2) != 100.0: frappe.throw(_("Percentage Allocation should be equal to 100%") + \ diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index edca210142a..2c6deb38960 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -309,7 +309,7 @@ class PaymentEntry(AccountsController): for k, v in no_oustanding_refs.items(): frappe.msgprint( _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") - .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")) + .format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount")) + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), title=_("Warning"), indicator="orange") @@ -524,7 +524,7 @@ class PaymentEntry(AccountsController): def set_unallocated_amount(self): self.unallocated_amount = 0 if self.party: - total_deductions = sum([flt(d.amount) for d in self.get("deductions")]) + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): @@ -549,7 +549,7 @@ class PaymentEntry(AccountsController): else: self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) - total_deductions = sum([flt(d.amount) for d in self.get("deductions")]) + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) self.difference_amount = flt(self.difference_amount - total_deductions, self.precision("difference_amount")) @@ -565,8 +565,8 @@ class PaymentEntry(AccountsController): if ((self.payment_type=="Pay" and self.party_type=="Customer") or (self.payment_type=="Receive" and self.party_type=="Supplier")): - total_negative_outstanding = sum([abs(flt(d.outstanding_amount)) - for d in self.get("references") if flt(d.outstanding_amount) < 0]) + total_negative_outstanding = sum(abs(flt(d.outstanding_amount)) + for d in self.get("references") if flt(d.outstanding_amount) < 0) paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount additional_charges = sum([flt(d.amount) for d in self.deductions]) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 468978785b4..438951db627 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -112,7 +112,7 @@ class PaymentRequest(Document): if not data_of_completed_requests: return self.grand_total - request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests]) + request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests) return request_amounts def on_cancel(self): diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index d23b952bdc2..b54d0e73a8c 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -20,9 +20,9 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money class MultiplePricingRuleConflict(frappe.ValidationError): pass apply_on_table = { - 'Item Code': 'items', - 'Item Group': 'item_groups', - 'Brand': 'brands' + 'Item Code': 'items', + 'Item Group': 'item_groups', + 'Brand': 'brands' } def get_pricing_rules(args, doc=None): @@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): condition = "ifnull({table}.{field}, '') in ({parent_groups})".format( table=table, field=field, - parent_groups=", ".join([frappe.db.escape(d) for d in parent_groups]) + parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups) ) frappe.flags.tree_conditions[key] = condition @@ -264,7 +264,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): # find pricing rule with highest priority if pricing_rules: - max_priority = max([cint(p.priority) for p in pricing_rules]) + max_priority = max(cint(p.priority) for p in pricing_rules) if max_priority: pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules)) @@ -272,14 +272,14 @@ def filter_pricing_rules(args, pricing_rules, doc=None): pricing_rules = list(pricing_rules) if len(pricing_rules) > 1: - rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules])) + rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules)) if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage": pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \ or pricing_rules if len(pricing_rules) > 1 and not args.for_shopping_cart: frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}") - .format("\n".join([d.name for d in pricing_rules])), MultiplePricingRuleConflict) + .format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict) elif pricing_rules: return pricing_rules[0] @@ -541,7 +541,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): if pricing_rule_args: - items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item]) + items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item) for args in pricing_rule_args: if not items or (args.get('item_code'), args.get('pricing_rules')) not in items: @@ -589,4 +589,4 @@ def update_coupon_code_count(coupon_name,transaction_type): elif transaction_type=='cancelled': if coupon.used>0: coupon.used=coupon.used-1 - coupon.save(ignore_permissions=True) \ No newline at end of file + coupon.save(ignore_permissions=True) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 723d151ad84..503dda77282 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -632,7 +632,7 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(len(pi.get("supplied_items")), 2) - rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")]) + rm_supp_cost = sum(d.amount for d in pi.get("supplied_items")) self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2)) def test_rejected_serial_no(self): 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 0cea7612dd3..dd26be7c992 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 @@ -112,7 +112,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): si = create_sales_invoice(customer = "Test TCS Customer", rate=5000) si.submit() - tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC') self.assertEqual(tcs_charged, 500) invoices.append(si) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index d4b249429bc..59009ae6215 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -143,7 +143,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): validate_expense_against_budget(args) def validate_cwip_accounts(gl_map): - cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) + cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")) if cwip_enabled and gl_map[0].voucher_type == "Journal Entry": cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index ff87276a876..c11c15390b3 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -32,7 +32,7 @@ def get_accounts_in_mappers(mapping_names): join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name where cfma.parent in (%s) order by cfm.is_working_capital - ''', (', '.join(['"%s"' % d for d in mapping_names]))) + ''', (', '.join('"%s"' % d for d in mapping_names))) def setup_mappers(mappers): @@ -83,8 +83,8 @@ def setup_mappers(mappers): account_types_labels = sorted( set( - [(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in account_types] + (d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense']) + for d in account_types ), key=lambda x: x[1] ) @@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) - accounts = ', '.join(['"%s"' % d for d in account_names]) + accounts = ', '.join('"%s"' % d for d in account_names) if opening_balances: date_info = dict(date=start_date) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 10b32fea562..c79d7401e6a 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object): out = [] for party, row in iteritems(self.party_data): if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount: - total_party_adjustment = sum([amount for amount in itervalues(self.party_adjustment_details.get(party, {}))]) + total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {}))) row.paid_amount -= total_party_adjustment adjustments = self.party_adjustment_details.get(party, {}) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index d20ddbde5c6..39ff8045181 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -369,7 +369,7 @@ def set_gl_entries_by_account( if accounts: additional_conditions += " and account in ({})"\ - .format(", ".join([frappe.db.escape(d) for d in accounts])) + .format(", ".join(frappe.db.escape(d) for d in accounts)) gl_filters = { "company": company, diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index cb4d9b43dbd..685419a17e4 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -334,7 +334,7 @@ def get_aii_accounts(): def get_purchase_receipts_against_purchase_order(item_list): po_pr_map = frappe._dict() - po_item_rows = list(set([d.po_detail for d in item_list])) + po_item_rows = list(set(d.po_detail for d in item_list)) if po_item_rows: purchase_receipts = frappe.db.sql(""" diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 928b373effe..2e794da8425 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -23,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) - mode_of_payments = get_mode_of_payments(set([d.parent for d in item_list])) + mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list)) so_dn_map = get_delivery_notes_against_sales_order(item_list) data = [] diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py index cfbd7fd0c8b..6a42bb4fb65 100644 --- a/erpnext/accounts/report/pos_register/pos_register.py +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field): ), filters, as_dict=1) def concat_mode_of_payments(pos_entries): - mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries])) + mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries)) for entry in pos_entries: if mode_of_payments.get(entry.pos_invoice): entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, [])) def add_subtotal_row(data, group_invoices, group_by_field, group_by_value): - grand_total = sum([d.grand_total for d in group_invoices]) - paid_amount = sum([d.paid_amount for d in group_invoices]) + grand_total = sum(d.grand_total for d in group_invoices) + paid_amount = sum(d.paid_amount for d in group_invoices) data.append({ group_by_field: group_by_value, "grand_total": grand_total, diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 8ac749d6290..10edd41aa88 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -26,7 +26,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) - suppliers = list(set([d.supplier for d in invoice_list])) + suppliers = list(set(d.supplier for d in invoice_list)) supplier_details = get_supplier_details(suppliers) company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") @@ -120,13 +120,13 @@ def get_columns(invoice_list, additional_table_columns): and docstatus = 1 and (account_head is not null and account_head != '') and category in ('Total', 'Valuation and Total') and parent in (%s) order by account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account from `tabPurchase Invoice` where docstatus = 1 and name in (%s) and ifnull(unrealized_profit_loss_account, '') != '' order by unrealized_profit_loss_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts] @@ -208,7 +208,7 @@ def get_invoice_expense_map(invoice_list): from `tabPurchase Invoice Item` where parent in (%s) group by parent, expense_account - """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_expense_map = {} for d in expense_details: @@ -221,7 +221,7 @@ def get_internal_invoice_map(invoice_list): unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, base_net_total as amount from `tabPurchase Invoice` where name in (%s) and is_internal_supplier = 1 and company = represents_company""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) internal_invoice_map = {} for d in unrealized_amount_details: @@ -238,7 +238,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): where parent in (%s) and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0 group by parent, account_head, add_deduct_tax - """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_tax_map = {} for d in tax_details: @@ -258,7 +258,7 @@ def get_invoice_po_pr_map(invoice_list): select parent, purchase_order, purchase_receipt, po_detail, project from `tabPurchase Invoice Item` where parent in (%s) - """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_po_pr_map = {} for d in pi_items: diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py index c234da0fe39..ff774681a29 100644 --- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py @@ -158,7 +158,7 @@ def get_sales_invoice_data(filters): def get_mode_of_payments(filters): mode_of_payments = {} invoice_list = get_invoices(filters) - invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list]) + invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) if invoice_list: inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment from `tabSales Invoice` a, `tabSales Invoice Payment` b @@ -197,7 +197,7 @@ def get_invoices(filters): def get_mode_of_payment_details(filters): mode_of_payment_details = {} invoice_list = get_invoices(filters) - invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list]) + invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) if invoice_list: inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index cb2c98b64ae..909959323f7 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -248,19 +248,19 @@ def get_columns(invoice_list, additional_table_columns): income_accounts = frappe.db.sql_list("""select distinct income_account from `tabSales Invoice Item` where docstatus = 1 and parent in (%s) order by income_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) tax_accounts = frappe.db.sql_list("""select distinct account_head from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice' and docstatus = 1 and base_tax_amount_after_discount_amount != 0 and parent in (%s) order by account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account from `tabSales Invoice` where docstatus = 1 and name in (%s) and ifnull(unrealized_profit_loss_account, '') != '' order by unrealized_profit_loss_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) for account in income_accounts: income_columns.append({ @@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns): def get_invoice_income_map(invoice_list): income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_income_map = {} for d in income_details: @@ -419,7 +419,7 @@ def get_internal_invoice_map(invoice_list): unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, base_net_total as amount from `tabSales Invoice` where name in (%s) and is_internal_customer = 1 and company = represents_company""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) internal_invoice_map = {} for d in unrealized_amount_details: @@ -432,7 +432,7 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): tax_details = frappe.db.sql("""select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_tax_map = {} for d in tax_details: @@ -451,7 +451,7 @@ def get_invoice_so_dn_map(invoice_list): si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail from `tabSales Invoice Item` where parent in (%s) and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_so_dn_map = {} for d in si_items: @@ -475,7 +475,7 @@ def get_invoice_cc_wh_map(invoice_list): si_items = frappe.db.sql("""select parent, cost_center, warehouse from `tabSales Invoice Item` where parent in (%s) and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_cc_wh_map = {} for d in si_items: diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index a8280c1b18e..e15715dccd8 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -78,7 +78,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f and company=%s and posting_date between %s and %s """, (supplier, company, from_date, to_date), as_dict=1) - supplier_credit_amount = flt(sum([d.credit for d in entries])) + supplier_credit_amount = flt(sum(d.credit for d in entries)) vouchers = [d.voucher_no for d in entries] vouchers += get_advance_vouchers([supplier], company=company, @@ -91,7 +91,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f from `tabGL Entry` where account=%s and posting_date between %s and %s and company=%s and credit > 0 and voucher_no in ({0}) - """.format(', '.join(["'%s'" % d for d in vouchers])), + """.format(', '.join("'%s'" % d for d in vouchers)), (account, from_date, to_date, company))[0][0]) date_range_filter = [fiscal_year, from_date, to_date] diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index b020d0a5062..ba461edaf86 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -139,6 +139,6 @@ def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=N gross_profit_data = GrossProfitGenerator(filters) result = gross_profit_data.grouped_data if not with_item_data: - result = sum([d.gross_profit for d in result]) + result = sum(d.gross_profit for d in result) return result diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5a64e27ccb7..66a9b601257 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -635,7 +635,7 @@ def get_held_invoices(party_type, party): 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()', as_dict=1 ) - held_invoices = set([d['name'] for d in held_invoices]) + held_invoices = set(d['name'] for d in held_invoices) return held_invoices diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 3cd4b802c11..8845f24d104 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase): }) asset.insert() accumulated_depreciation_after_full_schedule = \ - max([d.accumulated_depreciation_amount for d in asset.get("schedules")]) + max(d.accumulated_depreciation_amount for d in asset.get("schedules")) asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule)) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 14308277c14..2f6b5ee2dc9 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -92,7 +92,7 @@ class AssetValueAdjustment(Document): d.value_after_depreciation = asset_value if d.depreciation_method in ("Straight Line", "Manual"): - end_date = max([s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx]) + end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx) total_days = date_diff(end_date, self.date) rate_per_day = flt(d.value_after_depreciation) / flt(total_days) from_date = self.date diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 782593a5c59..2629ba7d61e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -139,7 +139,7 @@ class PurchaseOrder(BuyingController): def validate_minimum_order_qty(self): if not self.get("items"): return - items = list(set([d.item_code for d in self.get("items")])) + items = list(set(d.item_code for d in self.get("items"))) itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items)) @@ -326,10 +326,10 @@ class PurchaseOrder(BuyingController): so.notify_update() def has_drop_ship_item(self): - return any([d.delivered_by_supplier for d in self.items]) + return any(d.delivered_by_supplier for d in self.items) def is_against_so(self): - return any([d.sales_order for d in self.items if d.sales_order]) + return any(d.sales_order for d in self.items if d.sales_order) def set_received_qty_for_drop_ship_items(self): for item in self.items: diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 39171960d8c..3b9f8e9775a 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -359,7 +359,7 @@ class TestPurchaseOrder(unittest.TestCase): update_child_qty_rate('Purchase Order', trans_item, po.name) po.reload() - total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) + total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items")) self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 180ba936661..0127eb81630 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -391,7 +391,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc = def get_supplier_tag(): if not frappe.cache().hget("Supplier", "Tags"): filters = {"document_type": "Supplier"} - tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag])) + tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag)) frappe.cache().hset("Supplier", "Tags", tags) return frappe.cache().hget("Supplier", "Tags") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 53ded33b6f8..7c6061defab 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -610,8 +610,8 @@ class AccountsController(TransactionBase): order_field = "purchase_order" order_doctype = "Purchase Order" - order_list = list(set([d.get(order_field) - for d in self.get("items") if d.get(order_field)])) + order_list = list(set(d.get(order_field) + for d in self.get("items") if d.get(order_field))) journal_entries = get_advance_journal_entries(party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated) @@ -635,8 +635,8 @@ class AccountsController(TransactionBase): def validate_advance_entries(self): order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order" - order_list = list(set([d.get(order_field) - for d in self.get("items") if d.get(order_field)])) + order_list = list(set(d.get(order_field) + for d in self.get("items") if d.get(order_field))) if not order_list: return diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3f2d3390c05..da819119b10 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -181,8 +181,8 @@ class BuyingController(StockController): stock_and_asset_items_amount += flt(d.base_net_amount) last_item_idx = d.idx - total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") - if d.category in ["Valuation", "Valuation and Total"]]) + total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") + if d.category in ["Valuation", "Valuation and Total"]) valuation_amount_adjustment = total_valuation_amount for i, item in enumerate(self.get("items")): @@ -325,7 +325,7 @@ class BuyingController(StockController): def update_raw_materials_supplied_based_on_stock_entries(self): self.set('supplied_items', []) - purchase_orders = set([d.purchase_order for d in self.items]) + purchase_orders = set(d.purchase_order for d in self.items) # qty of raw materials backflushed (for each item per purchase order) backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 81ac234e700..7bd739a6ad3 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -88,7 +88,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Customer", fields) searchfields = frappe.get_meta("Customer").get_search_fields() - searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) + searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) return frappe.db.sql("""select {fields} from `tabCustomer` where docstatus < 2 diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 54156f379c5..7f28289760c 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -428,7 +428,7 @@ class SellingController(StockController): self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): - doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) + doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname))) if doc_list: po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')] diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 83d4c331401..943f7aaeb12 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -299,8 +299,8 @@ class StatusUpdater(Document): args['name'] = self.get(args['percent_join_field_parent']) self._update_percent_field(args, update_modified) else: - distinct_transactions = set([d.get(args['percent_join_field']) - for d in self.get_all_children(args['source_dt'])]) + distinct_transactions = set(d.get(args['percent_join_field']) + for d in self.get_all_children(args['source_dt'])) for name in distinct_transactions: if name: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 0da723d56ee..9c29b0076b3 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -313,7 +313,7 @@ class StockController(AccountsController): def get_serialized_items(self): serialized_items = [] - item_codes = list(set([d.item_code for d in self.get("items")])) + item_codes = list(set(d.item_code for d in self.get("items"))) if item_codes: serialized_items = frappe.db.sql_list("""select name from `tabItem` where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))), @@ -324,8 +324,8 @@ class StockController(AccountsController): def validate_warehouse(self): from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company - warehouses = list(set([d.warehouse for d in - self.get("items") if getattr(d, "warehouse", None)])) + warehouses = list(set(d.warehouse for d in + self.get("items") if getattr(d, "warehouse", None))) target_warehouses = list(set([d.target_warehouse for d in self.get("items") if getattr(d, "target_warehouse", None)])) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 0b4fb3a3ed5..2bb83ea7f0f 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -378,10 +378,10 @@ class calculate_taxes_and_totals(object): def manipulate_grand_total_for_inclusive_tax(self): # if fully inclusive taxes and diff - if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]): + if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")): last_tax = self.doc.get("taxes")[-1] - non_inclusive_tax_amount = sum([flt(d.tax_amount_after_discount_amount) - for d in self.doc.get("taxes") if not d.included_in_print_rate]) + non_inclusive_tax_amount = sum(flt(d.tax_amount_after_discount_amount) + for d in self.doc.get("taxes") if not d.included_in_print_rate) diff = self.doc.total + non_inclusive_tax_amount \ - flt(last_tax.total, last_tax.precision("total")) @@ -521,8 +521,8 @@ class calculate_taxes_and_totals(object): def calculate_total_advance(self): if self.doc.docstatus < 2: - total_allocated_amount = sum([flt(adv.allocated_amount, adv.precision("allocated_amount")) - for adv in self.doc.get("advances")]) + total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount")) + for adv in self.doc.get("advances")) self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance")) @@ -622,7 +622,7 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice" \ and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \ - and any([d.type == "Cash" for d in self.doc.payments]): + and any(d.type == "Cash" for d in self.doc.payments): grand_total = self.doc.rounded_total or self.doc.grand_total base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total diff --git a/erpnext/controllers/tests/test_mapper.py b/erpnext/controllers/tests/test_mapper.py index 66459fdbf8a..7a4b2d36148 100644 --- a/erpnext/controllers/tests/test_mapper.py +++ b/erpnext/controllers/tests/test_mapper.py @@ -26,8 +26,8 @@ class TestMapper(unittest.TestCase): # Assert that all inserted items are present in updated sales order src_items = item_list_1 + item_list_2 + item_list_3 - self.assertEqual(set([d for d in src_items]), - set([d.item_code for d in updated_so.items])) + self.assertEqual(set(d for d in src_items), + set(d.item_code for d in updated_so.items)) def make_quotation(self, item_list, customer): diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index ecf041efd17..7c072e4fad3 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -113,7 +113,7 @@ def post_process(doctype, data): doc.set_indicator() doc.status_display = ", ".join(doc.status_display) - doc.items_preview = ", ".join([d.item_name for d in doc.items if d.item_name]) + doc.items_preview = ", ".join(d.item_name for d in doc.items if d.item_name) result.append(doc) return result diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d764db33f86..cdc45188942 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -317,7 +317,7 @@ class JobCard(Document): 'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id') if job_cards: - qty = min([d.qty for d in job_cards]) + qty = min(d.qty for d in job_cards) doc.db_set('material_transferred_for_manufacturing', qty) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index d1583f1473c..39a6024e2cc 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -232,7 +232,7 @@ def get_project(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta(doctype) searchfields = meta.get_search_fields() search_columns = ", " + ", ".join(searchfields) if searchfields else '' - search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields]) + search_cond = " or " + " or ".join(field + " like %(txt)s" for field in searchfields) return frappe.db.sql(""" select name {search_columns} from `tabProject` where %(key)s like %(txt)s diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index a3e4577f909..c8bd80fca0a 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -87,8 +87,8 @@ class Timesheet(Document): def set_dates(self): if self.docstatus < 2 and self.time_logs: - start_date = min([getdate(d.from_time) for d in self.time_logs]) - end_date = max([getdate(d.to_time) for d in self.time_logs]) + start_date = min(getdate(d.from_time) for d in self.time_logs) + end_date = max(getdate(d.to_time) for d in self.time_logs) if start_date and end_date: self.start_date = getdate(start_date) diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py index 2c7bb49cfba..98dd617f9b3 100644 --- a/erpnext/projects/report/project_summary/project_summary.py +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -122,7 +122,7 @@ def get_report_summary(data): if not data: return None - avg_completion = sum([project.percent_complete for project in data]) / len(data) + avg_completion = sum(project.percent_complete for project in data) / len(data) total = sum([project.total_tasks for project in data]) total_overdue = sum([project.overdue_tasks for project in data]) completed = sum([project.completed_tasks for project in data]) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 51d86ff0bf8..818888c0c12 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -75,7 +75,7 @@ class Customer(TransactionBase): self.loyalty_program_tier = customer.loyalty_program_tier if self.sales_team: - if sum([member.allocated_percentage or 0 for member in self.sales_team]) != 100: + if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) def check_customer_group_change(self): diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 246f9234a41..e4f8a475816 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -50,7 +50,7 @@ class Quotation(SellingController): self.customer_name = company_name or lead_name def update_opportunity(self, status): - for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])): + for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: self.update_opportunity_status(status, opportunity) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index d9e52e1d69d..551f715bd52 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -151,7 +151,7 @@ class SalesOrder(SellingController): frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0])) def update_prevdoc_status(self, flag=None): - for quotation in list(set([d.prevdoc_docname for d in self.get("items")])): + for quotation in set(d.prevdoc_docname for d in self.get("items")): if quotation: doc = frappe.get_doc("Quotation", quotation) if doc.docstatus==2: diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 077538d479c..27e023c1e5b 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -54,7 +54,7 @@ class Company(NestedSet): def validate_abbr(self): if not self.abbr: - self.abbr = ''.join([c[0] for c in self.company_name.split()]).upper() + self.abbr = ''.join(c[0] for c in self.company_name.split()).upper() self.abbr = self.abbr.strip() @@ -335,7 +335,7 @@ class Company(NestedSet): clear_defaults_cache() def abbreviate(self): - self.abbr = ''.join([c[0].upper() for c in self.company_name.split()]) + self.abbr = ''.join(c[0].upper() for c in self.company_name.split()) def on_trash(self): """ diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index bff806d5472..db31d6d6992 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -139,7 +139,7 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non # return child item groups if the type is of "Is Group" return get_child_groups_for_list_in_html(item_group, start, limit, search) - child_groups = ", ".join([frappe.db.escape(i[0]) for i in get_child_groups(product_group)]) + child_groups = ", ".join(frappe.db.escape(i[0]) for i in get_child_groups(product_group)) # base query query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group, @@ -239,7 +239,7 @@ def get_item_for_list_in_html(context): return frappe.get_template(products_template).render(context) def get_group_item_count(item_group): - child_groups = ", ".join(['"' + i[0] + '"' for i in get_child_groups(item_group)]) + child_groups = ", ".join('"' + i[0] + '"' for i in get_child_groups(item_group)) return frappe.db.sql("""select count(*) from `tabItem` where docstatus = 0 and show_in_website = 1 and (item_group in (%s) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 8fdda565d20..508e17c3409 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -304,7 +304,7 @@ def validate_serial_no_with_batch(serial_nos, item_code): frappe.throw(_("The serial no {0} does not belong to item {1}") .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code))) - serial_no_link = ','.join([get_link_to_form("Serial No", sn) for sn in serial_nos]) + serial_no_link = ','.join(get_link_to_form("Serial No", sn) for sn in serial_nos) message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" frappe.throw(_("There is no batch found against the {0}: {1}") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index cce51cb9b17..dd31965face 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -264,7 +264,7 @@ class DeliveryNote(SellingController): """ Validate that if packed qty exists, it should be equal to qty """ - if not any([flt(d.get('packed_qty')) for d in self.get("items")]): + if not any(flt(d.get('packed_qty')) for d in self.get("items")): return has_error = False for d in self.get("items"): diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 81e730126ec..9ec28d89814 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -68,7 +68,7 @@ class DeliveryTrip(Document): delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. """ - delivery_notes = list(set([stop.delivery_note for stop in self.delivery_stops if stop.delivery_note])) + delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)) update_fields = { "driver": self.driver, @@ -136,8 +136,8 @@ class DeliveryTrip(Document): # Include last leg in the final distance calculation self.uom = self.default_distance_uom - total_distance = sum([leg.get("distance", {}).get("value", 0.0) - for leg in directions.get("legs")]) # in meters + total_distance = sum(leg.get("distance", {}).get("value", 0.0) + for leg in directions.get("legs")) # in meters self.total_distance = total_distance * self.uom_conversion_factor else: idx += len(route) - 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 83109469fc7..5df4d8743fc 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -78,7 +78,7 @@ class LandedCostVoucher(Document): .format(item.idx, item.item_code)) def set_total_taxes_and_charges(self): - self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")]) + self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes")) def set_applicable_charges_on_item(self): if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': @@ -104,15 +104,15 @@ class LandedCostVoucher(Document): based_on = self.distribute_charges_based_on.lower() if based_on != 'distribute manually': - total = sum([flt(d.get(based_on)) for d in self.get("items")]) + total = sum(flt(d.get(based_on)) for d in self.get("items")) else: # consider for proportion while distributing manually - total = sum([flt(d.get('applicable_charges')) for d in self.get("items")]) + total = sum(flt(d.get('applicable_charges')) for d in self.get("items")) if not total: frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) - total_applicable_charges = sum([flt(d.applicable_charges) for d in self.get("items")]) + total_applicable_charges = sum(flt(d.applicable_charges) for d in self.get("items")) precision = get_field_precision(frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), currency=frappe.get_cached_value('Company', self.company, "default_currency")) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 984ae46c66c..32b08f60c4a 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -311,7 +311,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, def distribute_landed_cost_on_items(lcv): based_on = lcv.distribute_charges_based_on.lower() - total = sum([flt(d.get(based_on)) for d in lcv.get("items")]) + total = sum(flt(d.get(based_on)) for d in lcv.get("items")) for item in lcv.get("items"): item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 2008bffcd32..4a843e0fde9 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -88,9 +88,9 @@ class PackingSlip(Document): rows = [d.item_code for d in self.get("items")] # also pick custom fields from delivery note - custom_fields = ', '.join(['dni.`{0}`'.format(d.fieldname) + custom_fields = ', '.join('dni.`{0}`'.format(d.fieldname) for d in frappe.get_meta("Delivery Note Item").get_custom_fields() - if d.fieldtype not in no_value_fields]) + if d.fieldtype not in no_value_fields) if custom_fields: custom_fields = ', ' + custom_fields diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 5095a80214d..8d9b675bed3 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -246,7 +246,7 @@ class TestPurchaseReceipt(unittest.TestCase): pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes") self.assertEqual(len(pr.get("supplied_items")), 2) - rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) + rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) pr.cancel() diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 5ecc9f81405..b236f6a9990 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -613,7 +613,7 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]): batch_nos = filters.get("batch_no") expiry_date = filters.get("expiry_date") if batch_nos: - batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join(["'%s'" % d for d in batch_nos])) + batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos)) if expiry_date: batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name " diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2f76bc7d56a..560ceaa917b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -465,7 +465,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item]) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) # Set basic rate for incoming items for d in self.get('items'): diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 5296211fae8..db7498bb215 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -89,7 +89,7 @@ def get_item_price_qty_data(filters): {conditions}""" .format(conditions=conditions), filters, as_dict=1) - price_list_names = list(set([item.price_list_name for item in item_results])) + price_list_names = list(set(item.price_list_name for item in item_results)) buying_price_map = get_price_map(price_list_names, buying=1) selling_price_map = get_price_map(price_list_names, selling=1) diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index 2f70523264a..2e13aa0b040 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -66,7 +66,7 @@ def get_consumed_items(condition): purpose is NULL or purpose not in ({}) ) - """.format(', '.join([f"'{p}'" for p in purpose_to_exclude])) + """.format(', '.join(f"'{p}'" for p in purpose_to_exclude)) condition = condition.replace("posting_date", "sle.posting_date") consumed_items = frappe.db.sql(""" diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 276e42ee433..8fffbccab3c 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -141,7 +141,7 @@ def get_stock_ledger_entries(filters, items): return [] item_conditions_sql = ' and sle.item_code in ({})' \ - .format(', '.join([frappe.db.escape(i) for i in items])) + .format(', '.join(frappe.db.escape(i) for i in items)) conditions = get_sle_conditions(filters) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index bbd73e91129..b6a80631892 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -157,7 +157,7 @@ def get_stock_ledger_entries(filters, items): item_conditions_sql = '' if items: item_conditions_sql = ' and sle.item_code in ({})'\ - .format(', '.join([frappe.db.escape(i, percent=False) for i in items])) + .format(', '.join(frappe.db.escape(i, percent=False) for i in items)) conditions = get_conditions(filters) @@ -253,7 +253,7 @@ def get_items(filters): def get_item_details(items, sle, filters): item_details = {} if not items: - items = list(set([d.item_code for d in sle])) + items = list(set(d.item_code for d in sle)) if not items: return item_details @@ -291,7 +291,7 @@ def get_item_reorder_details(items): select parent, warehouse, warehouse_reorder_qty, warehouse_reorder_level from `tabItem Reorder` where parent in ({0}) - """.format(', '.join([frappe.db.escape(i, percent=False) for i in items])), as_dict=1) + """.format(', '.join(frappe.db.escape(i, percent=False) for i in items)), as_dict=1) return dict((d.parent + d.warehouse, d) for d in item_reorder_details) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 36996e96745..8909f217f49 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -115,7 +115,7 @@ def get_stock_ledger_entries(filters, items): item_conditions_sql = '' if items: item_conditions_sql = 'and sle.item_code in ({})'\ - .format(', '.join([frappe.db.escape(i) for i in items])) + .format(', '.join(frappe.db.escape(i) for i in items)) sl_entries = frappe.db.sql(""" SELECT @@ -169,7 +169,7 @@ def get_items(filters): def get_item_details(items, sl_entries, include_uom): item_details = {} if not items: - items = list(set([d.item_code for d in sl_entries])) + items = list(set(d.item_code for d in sl_entries)) if not items: return item_details diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b2825fc26f5..fc82c789cc7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -521,7 +521,7 @@ class update_entries_after(object): fields=["purchase_rate", "name", "company"], filters = {'name': ('in', serial_nos)}) - incoming_values = sum([flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company]) + incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company) # Get rate for serial nos which has been transferred to other company invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company] diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py index a3428a25af4..a20e7a801b0 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py @@ -29,7 +29,7 @@ class WarrantyClaim(TransactionBase): where t2.parent = t1.name and t2.prevdoc_docname = %s and t1.docstatus!=2""", (self.name)) if lst: - lst1 = ','.join([x[0] for x in lst]) + lst1 = ','.join(x[0] for x in lst) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) else: frappe.db.set(self, 'status', 'Cancelled') diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 66d6cd38886..70b41767d6d 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -131,6 +131,6 @@ def get_non_stock_item_status(item_code, item_warehouse_field): if frappe.db.exists("Product Bundle", item_code): items = frappe.get_doc("Product Bundle", item_code).get_all_children() bundle_warehouse = frappe.db.get_value('Item', item_code, item_warehouse_field) - return all([ get_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items ]) + return all(get_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items) else: return 1 diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index f99da58e467..db997263c1a 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -147,7 +147,7 @@ class TransactionBase(StatusUpdater): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): fieldname = self.prev_link_mapper[for_doctype]["fieldname"] - values = filter(None, tuple([item.as_dict()[fieldname] for item in self.items])) + values = filter(None, tuple(item.as_dict()[fieldname] for item in self.items)) if values: ret = { @@ -180,7 +180,7 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, string_types): qty_fields = [qty_fields] - distinct_uoms = list(set([d.get(uom_field) for d in doc.get_all_children()])) + distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) integer_uoms = list(filter(lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, distinct_uoms)) From c4d851e45f591667a33e3fb7e50c5bf1cf14a993 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Jun 2021 17:27:43 +0530 Subject: [PATCH 066/376] fix: Test --- ...tracted_raw_materials_to_be_transferred.py | 68 ++++++++++++++----- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index c1fc6fb82ff..11ec7669b0c 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -12,34 +12,68 @@ import json, frappe, unittest class TestSubcontractedItemToBeTransferred(unittest.TestCase): def test_pending_and_transferred_qty(self): - po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') + po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC") + + # Material Receipt of RMs make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100) make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100) - transfer_subcontracted_raw_materials(po.name) - col, data = execute(filters=frappe._dict({'supplier': po.supplier, - 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)), - 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))})) - self.assertEqual(data[0]['purchase_order'], po.name) - self.assertIn(data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) - self.assertIn(data[0]['p_qty'], [9, 18]) - self.assertIn(data[0]['t_qty'], [1, 2]) + + se = transfer_subcontracted_raw_materials(po) + + col, data = execute(filters=frappe._dict( + { + 'supplier': po.supplier, + 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)), + 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)) + } + )) + po.reload() + + po_data = [row for row in data if row.get('purchase_order') == po.name] + + self.assertEqual(len(po_data), 2) + self.assertIn(po_data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) + self.assertIn(po_data[0]['p_qty'], [9, 18]) + self.assertIn(po_data[0]['transferred_qty'], [1, 2]) self.assertEqual(data[1]['purchase_order'], po.name) self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) self.assertIn(data[1]['p_qty'], [9, 18]) - self.assertIn(data[1]['t_qty'], [1, 2]) + self.assertIn(data[1]['transferred_qty'], [1, 2]) + se.cancel() + po.cancel() def transfer_subcontracted_raw_materials(po): rm_item = [ - {'item_code': '_Test Item', 'rm_item_code': '_Test Item', 'item_name': '_Test Item', 'qty': 1, - 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100, 'stock_uom': 'Nos'}, - {'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2, - 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}] + { + 'name': po.supplied_items[0].name, + 'item_code': '_Test Item Home Desktop 100', + 'rm_item_code': '_Test Item Home Desktop 100', + 'item_name': '_Test Item Home Desktop 100', + 'qty': 2, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 200, + 'stock_uom': 'Nos' + }, + { + 'name': po.supplied_items[1].name, + 'item_code': '_Test Item', + 'rm_item_code': '_Test Item', + 'item_name': '_Test Item', + 'qty': 1, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 100, + 'stock_uom': 'Nos' + } + ] rm_item_string = json.dumps(rm_item) - se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string)) - se.from_warehouse = '_Test Warehouse 1 - _TC' - se.to_warehouse = '_Test Warehouse 1 - _TC' + se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + se.from_warehouse = '_Test Warehouse - _TC' + se.to_warehouse = '_Test Warehouse - _TC' se.stock_entry_type = 'Send to Subcontractor' se.save() se.submit() + return se From 400205cc8a9632614617af2f21d598491db8cde8 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Sat, 12 Jun 2021 12:30:53 +0530 Subject: [PATCH 067/376] fix: payroll entry employee detail issue (#25968) * fix: payroll entry employee detail issue * fix: review changes --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 3953b463f12..7a70679db47 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -665,6 +665,8 @@ def get_employee_list(filters): emp_list = remove_payrolled_employees(emp_list, filters.start_date, filters.end_date) return emp_list + return [] + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): From 17550fb4bfb968ea5c0be9ab1e233ac55ed35936 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 12 Jun 2021 13:33:21 +0530 Subject: [PATCH 068/376] feat: add Inactive status to Employee --- erpnext/accounts/party.py | 2 +- erpnext/hr/doctype/employee/employee.json | 4 ++-- erpnext/hr/doctype/employee/employee.py | 4 ++-- erpnext/hr/doctype/employee/employee_list.js | 2 +- erpnext/hr/doctype/employee_promotion/employee_promotion.py | 6 +++--- erpnext/hr/doctype/employee_transfer/employee_transfer.py | 6 +++--- erpnext/payroll/doctype/retention_bonus/retention_bonus.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e01cb6e151e..e025fc69054 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -457,7 +457,7 @@ def validate_party_frozen_disabled(party_type, party_name): frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen) elif party_type == "Employee": - if frappe.db.get_value("Employee", party_name, "status") == "Left": + if frappe.db.get_value("Employee", party_name, "status") != "Active": frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True) def get_timeline_data(doctype, name): diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 5123d6a5a78..5442ed56c31 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -207,7 +207,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Active\nLeft", + "options": "Active\nInactive\nLeft", "reqd": 1, "search_index": 1 }, @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2021-01-02 16:54:33.477439", + "modified": "2021-06-12 11:31:37.730760", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index ed7d5884347..bc5694226ad 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -37,7 +37,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Active", "Temporary Leave", "Left"]) + validate_status(self.status, ["Active", "Inactive", "Left"]) self.employee = self.name self.set_employee_name() @@ -478,7 +478,7 @@ def get_employee_emails(employee_list): @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [['status', '!=', 'Left']] + filters = [['status', '=', 'Active']] if company and company != 'All Companies': filters.append(['company', '=', company]) diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js index 44837030be8..6679e318c24 100644 --- a/erpnext/hr/doctype/employee/employee_list.js +++ b/erpnext/hr/doctype/employee/employee_list.js @@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = { filters: [["status","=", "Active"]], get_indicator: function(doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Temporary Leave": "red", "Left": "gray"}[doc.status]; + indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; return indicator; } }; diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py index 49949212689..83fb235f92c 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee class EmployeePromotion(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") == "Left": - frappe.throw(_("Cannot promote Employee with status Left")) + if frappe.get_value("Employee", self.employee, "status") != "Active": + frappe.throw(_("Cannot promote Employee with status Left or Inactive")) def before_submit(self): if getdate(self.promotion_date) > getdate(): - frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date "), + frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date"), frappe.DocstatusTransitionError) def on_submit(self): diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py index 3539970a32a..6eec9fa12a9 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee class EmployeeTransfer(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") == "Left": - frappe.throw(_("Cannot transfer Employee with status Left")) + if frappe.get_value("Employee", self.employee, "status") != "Active": + frappe.throw(_("Cannot transfer Employee with status Left or Inactive")) def before_submit(self): if getdate(self.transfer_date) > getdate(): - frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date "), + frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), frappe.DocstatusTransitionError) def on_submit(self): diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py index b8e56ae42aa..049ea265cce 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -10,8 +10,8 @@ from frappe.utils import getdate class RetentionBonus(Document): def validate(self): - if frappe.get_value('Employee', self.employee, 'status') == 'Left': - frappe.throw(_('Cannot create Retention Bonus for left Employees')) + if frappe.get_value('Employee', self.employee, 'status') != 'Active': + frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees')) if getdate(self.bonus_payment_date) < getdate(): frappe.throw(_('Bonus Payment Date cannot be a past date')) From cf349aadf9ea7108bb0a95b86da23ffab87d62e5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 12 Jun 2021 14:05:12 +0530 Subject: [PATCH 069/376] fix: unable to enter score in Assessment Result details grid (#25945) (#26031) --- .../education/doctype/assessment_result/assessment_result.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/education/doctype/assessment_result/assessment_result.js b/erpnext/education/doctype/assessment_result/assessment_result.js index 617a873b820..c35f607a99d 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result.js +++ b/erpnext/education/doctype/assessment_result/assessment_result.js @@ -6,7 +6,8 @@ frappe.ui.form.on('Assessment Result', { if (!frm.doc.__islocal) { frm.trigger('setup_chart'); } - frm.set_df_property('details', 'read_only', 1); + + frm.get_field('details').grid.cannot_add_rows = true; frm.set_query('course', function() { return { From 28cdff10cfe981c6e91b1a9ef897638e7c800af6 Mon Sep 17 00:00:00 2001 From: Anoop <3326959+akurungadam@users.noreply.github.com> Date: Sat, 12 Jun 2021 14:17:04 +0530 Subject: [PATCH 070/376] fix: update linked Customer on Patient update only if Link Customer to Patient is enabled (#25926) --- erpnext/healthcare/doctype/patient/patient.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 789d452c070..cebcb2068ea 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -33,21 +33,21 @@ class Patient(Document): self.reload() # self.notify_update() def on_update(self): - if self.customer: - customer = frappe.get_doc('Customer', self.customer) - if self.customer_group: - customer.customer_group = self.customer_group - if self.territory: - customer.territory = self.territory + if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'): + if self.customer: + customer = frappe.get_doc('Customer', self.customer) + if self.customer_group: + customer.customer_group = self.customer_group + if self.territory: + customer.territory = self.territory - customer.customer_name = self.patient_name - customer.default_price_list = self.default_price_list - customer.default_currency = self.default_currency - customer.language = self.language - customer.ignore_mandatory = True - customer.save(ignore_permissions=True) - else: - if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'): + customer.customer_name = self.patient_name + customer.default_price_list = self.default_price_list + customer.default_currency = self.default_currency + customer.language = self.language + customer.ignore_mandatory = True + customer.save(ignore_permissions=True) + else: create_customer(self) def set_full_name(self): From 580346360fc3a6dd168e726cd035f5cc42f8f3c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 11:16:39 +0530 Subject: [PATCH 071/376] fix: Auto tax calculations in Payment Entry --- .../doctype/payment_entry/payment_entry.js | 86 +++++++++++++++++-- .../doctype/payment_entry/payment_entry.py | 44 +++++----- .../purchase_taxes_and_charges.json | 3 +- .../sales_taxes_and_charges.json | 3 +- erpnext/controllers/accounts_controller.py | 34 ++++---- 5 files changed, 118 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 939f3546ff6..d3ac3a66760 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1087,6 +1087,8 @@ frappe.ui.form.on('Payment Entry', { initialize_taxes: function(frm) { $.each(frm.doc["taxes"] || [], function(i, tax) { + frm.events.validate_taxes_and_charges(tax); + frm.events.validate_inclusive_tax(tax); tax.item_wise_tax_detail = {}; let tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]; @@ -1101,6 +1103,73 @@ frappe.ui.form.on('Payment Entry', { }); }, + validate_taxes_and_charges: function(d) { + let msg = ""; + + if (d.account_head && !d.description) { + // set description from account head + d.description = d.account_head.split(' - ').slice(0, -1).join(' - '); + } + + if (!d.charge_type && (d.row_id || d.rate || d.tax_amount)) { + msg = __("Please select Charge Type first"); + d.row_id = ""; + d.rate = d.tax_amount = 0.0; + } else if ((d.charge_type == 'Actual' || d.charge_type == 'On Net Total' || d.charge_type == 'On Paid Amount') && d.row_id) { + msg = __("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"); + d.row_id = ""; + } else if ((d.charge_type == 'On Previous Row Amount' || d.charge_type == 'On Previous Row Total') && d.row_id) { + if (d.idx == 1) { + msg = __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"); + d.charge_type = ''; + } else if (!d.row_id) { + msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]); + d.row_id = ""; + } else if (d.row_id && d.row_id >= d.idx) { + msg = __("Cannot refer row number greater than or equal to current row number for this Charge type"); + d.row_id = ""; + } + } + if (msg) { + frappe.validated = false; + refresh_field("taxes"); + frappe.throw(msg); + } + + }, + + validate_inclusive_tax: function(tax) { + let actual_type_error = function() { + let msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx]) + frappe.throw(msg); + }; + + let on_previous_row_error = function(row_range) { + let msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included", + [tax.idx, __(tax.doctype), tax.charge_type, row_range]) + frappe.throw(msg); + }; + + if(cint(tax.included_in_paid_amount)) { + if(tax.charge_type == "Actual") { + // inclusive tax cannot be of type Actual + actual_type_error(); + } else if(tax.charge_type == "On Previous Row Amount" && + !cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_paid_amount) + ) { + // referred row should also be an inclusive tax + on_previous_row_error(tax.row_id); + } else if(tax.charge_type == "On Previous Row Total") { + let taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id), + function(t) { return cint(t.included_in_paid_amount) ? null : t; }); + if(taxes_not_included.length > 0) { + // all rows above this tax should be inclusive + on_previous_row_error(tax.row_id == 1 ? "1" : "1 - " + tax.row_id); + } + } + } + }, + determine_exclusive_rate: function(frm) { let has_inclusive_tax = false; $.each(frm.doc["taxes"] || [], function(i, row) { @@ -1110,8 +1179,7 @@ frappe.ui.form.on('Payment Entry', { let cumulated_tax_fraction = 0.0; $.each(frm.doc["taxes"] || [], function(i, tax) { - let current_tax_fraction = frm.events.get_current_tax_fraction(frm, tax); - tax.tax_fraction_for_current_item = current_tax_fraction[0]; + tax.tax_fraction_for_current_item = frm.events.get_current_tax_fraction(frm, tax); if(i==0) { tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item; @@ -1132,9 +1200,7 @@ frappe.ui.form.on('Payment Entry', { if(cint(tax.included_in_paid_amount)) { let tax_rate = tax.rate; - if (tax.charge_type == "Actual") { - current_tax_fraction = tax.tax_amount/(frm.doc.paid_amount_after_tax + frm.doc.tax_amount); - } else if(tax.charge_type == "On Paid Amount") { + if(tax.charge_type == "On Paid Amount") { current_tax_fraction = (tax_rate / 100.0); } else if(tax.charge_type == "On Previous Row Amount") { current_tax_fraction = (tax_rate / 100.0) * @@ -1147,7 +1213,6 @@ frappe.ui.form.on('Payment Entry', { if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") { current_tax_fraction *= -1; - inclusive_tax_amount_per_qty *= -1; } return current_tax_fraction; }, @@ -1207,10 +1272,8 @@ frappe.ui.form.on('Payment Entry', { frappe.throw( __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row")); } - if (!tax.row_id) { - tax.row_id = tax.idx - 1; - } } + if(tax.charge_type == "Actual") { current_tax_amount = flt(tax.tax_amount, precision("tax_amount", tax)) } else if(tax.charge_type == "On Paid Amount") { @@ -1296,6 +1359,11 @@ frappe.ui.form.on('Advance Taxes and Charges', { included_in_paid_amount: function(frm) { frm.events.apply_taxes(frm); frm.events.set_unallocated_amount(frm); + }, + + charge_type: function(frm) { + frm.events.apply_taxes(frm); + frm.events.set_unallocated_amount(frm); } }) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2c6deb38960..70b38735e7c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -16,9 +16,11 @@ from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_ac from erpnext.controllers.accounts_controller import AccountsController, get_supplier_block_status from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details - from six import string_types, iteritems +from erpnext.controllers.accounts_controller import validate_conversion_rate, \ + validate_taxes_and_charges, validate_inclusive_tax + class InvalidPaymentEntry(ValidationError): pass @@ -407,20 +409,6 @@ class PaymentEntry(AccountsController): net_total = self.paid_amount included_in_paid_amount = 0 - if self.get('references'): - for doc in self.get('references'): - if doc.reference_doctype == 'Purchase Order': - reference_doclist.append(doc.reference_name) - - if reference_doclist: - order_amount = frappe.db.get_all('Purchase Order', fields=['sum(net_total)'], - filters = {'name': ('in', reference_doclist), 'docstatus': 1, - 'apply_tds': 1}, as_list=1) - - if order_amount: - net_total = order_amount[0][0] - included_in_paid_amount = 1 - # Adding args as purchase invoice to get TDS amount args = frappe._dict({ 'company': self.company, @@ -719,9 +707,9 @@ class PaymentEntry(AccountsController): if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if (self.payment_type == 'Pay' and self.advance_tax_account) or self.payment_type == 'Receive': + if self.payment_type == 'Pay': dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" - elif (self.payment_type == 'Receive' and self.advance_tax_account) or self.payment_type == 'Pay': + elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" payment_or_advance_account = self.get_party_account_for_taxes() @@ -747,6 +735,8 @@ class PaymentEntry(AccountsController): if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, + "party_type": self.party_type, + "party": self.party }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -770,9 +760,9 @@ class PaymentEntry(AccountsController): def get_party_account_for_taxes(self): if self.advance_tax_account: return self.advance_tax_account - elif self.payment_type == 'Pay': - return self.paid_from elif self.payment_type == 'Receive': + return self.paid_from + elif self.payment_type == 'Pay': return self.paid_to def update_advance_paid(self): @@ -823,6 +813,9 @@ class PaymentEntry(AccountsController): def initialize_taxes(self): for tax in self.get("taxes"): + validate_taxes_and_charges(tax) + validate_inclusive_tax(tax, self) + tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] if tax.charge_type != "Actual": @@ -918,15 +911,11 @@ class PaymentEntry(AccountsController): if cint(tax.included_in_paid_amount): tax_rate = tax.rate - if tax.charge_type == 'Actual': - current_tax_fraction = tax.tax_amount/ (self.paid_amount_after_tax + tax.tax_amount) - elif tax.charge_type == "On Paid Amount": + if tax.charge_type == "On Paid Amount": current_tax_fraction = tax_rate / 100.0 - elif tax.charge_type == "On Previous Row Amount": current_tax_fraction = (tax_rate / 100.0) * \ self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item - elif tax.charge_type == "On Previous Row Total": current_tax_fraction = (tax_rate / 100.0) * \ self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item @@ -1626,6 +1615,13 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta paid_amount = received_amount * doc.get('conversion_rate', 1) if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) + + if dt == "Purchase Order" and doc.apply_tds: + if party_account_currency == bank.account_currency: + paid_amount = received_amount = doc.base_net_total + else: + paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) + return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 9b07645ccc9..1fa68e0a8a8 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -218,6 +218,7 @@ }, { "default": "0", + "depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry", "fieldname": "included_in_paid_amount", "fieldtype": "Check", @@ -227,7 +228,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:48:25.335733", + "modified": "2021-06-14 01:43:50.750455", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 170d34e651c..1b7a0fe562e 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -195,6 +195,7 @@ }, { "default": "0", + "depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry", "fieldname": "included_in_paid_amount", "fieldtype": "Check", @@ -205,7 +206,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:48:04.691596", + "modified": "2021-06-14 01:44:36.899147", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7c6061defab..a507159cb65 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1192,7 +1192,7 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c def validate_taxes_and_charges(tax): - if tax.charge_type in ['Actual', 'On Net Total'] and tax.row_id: + if tax.charge_type in ['Actual', 'On Net Total', 'On Paid Amount'] and tax.row_id: frappe.throw(_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'")) elif tax.charge_type in ['On Previous Row Amount', 'On Previous Row Total']: if cint(tax.idx) == 1: @@ -1209,23 +1209,23 @@ def validate_taxes_and_charges(tax): def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): - throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, - row_range)) + throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) - if cint(getattr(tax, "included_in_print_rate", None)): - if tax.charge_type == "Actual": - # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate): - # referred row should also be inclusive - _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]): - # all rows about the reffered tax should be inclusive - _on_previous_row_error("1 - %d" % (tax.row_id,)) - elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + for fieldname in ['included_in_print_rate', 'included_in_paid_amount']: + if cint(getattr(tax, fieldname, None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].get(fieldname)): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.get(fieldname) for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): From 0511ffcf30459a8646978429101c7edf6315f69b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 14 Jun 2021 13:22:44 +0530 Subject: [PATCH 072/376] fix(General Ledger): Implement multi-account selection --- .../report/general_ledger/general_ledger.js | 15 +++++------- .../report/general_ledger/general_ledger.py | 24 ++++++++++++++----- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 84f786814de..f3c3865b4e2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -36,16 +36,13 @@ frappe.query_reports["General Ledger"] = { { "fieldname":"account", "label": __("Account"), - "fieldtype": "Link", + "fieldtype": "MultiSelectList", "options": "Account", - "get_query": function() { - var company = frappe.query_report.get_filter_value('company'); - return { - "doctype": "Account", - "filters": { - "company": company, - } - } + get_data: function(txt) { + console.log("txt = ", txt) + return frappe.db.get_link_options('Account', txt, { + company: frappe.query_report.get_filter_value("company") + }); } }, { diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 562df4f6f7d..53c638bf4a0 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -49,8 +49,12 @@ def validate_filters(filters, account_details): if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - if filters.get("account") and not account_details.get(filters.account): - frappe.throw(_("Account {0} does not exists").format(filters.account)) + for account in filters.account: + if not account_details.get(account): + frappe.throw(_("Account {0} does not exists").format(account)) + + if filters.get('account'): + filters.account = frappe.parse_json(filters.get('account')) if (filters.get("account") and filters.get("group_by") == _('Group by Account') and account_details[filters.account].is_group == 0): @@ -87,7 +91,7 @@ def set_account_currency(filters): account_currency = None if filters.get("account"): - account_currency = get_account_currency(filters.account) + account_currency = get_account_currency(filters.account[0]) elif filters.get("party"): gle_currency = frappe.db.get_value( "GL Entry", { @@ -205,10 +209,18 @@ def get_gl_entries(filters, accounting_dimensions): def get_conditions(filters): conditions = [] + if filters.get("account") and not filters.get("include_dimensions"): - lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) - conditions.append("""account in (select name from tabAccount - where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + account_conditions = "" + for account in filters["account"]: + lft, rgt = frappe.db.get_value("Account", account, ["lft", "rgt"]) + account_conditions += """account in (select name from tabAccount + where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt) + + # so that the OR doesn't get added to the last account condition + if account != filters["account"][-1]: + account_conditions += " OR " + conditions.append(account_conditions) if filters.get("cost_center"): filters.cost_center = get_cost_centers_with_children(filters.cost_center) From 8718013c96441a7ab71b22bc97fe9bc06bdb01d7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 14:34:44 +0530 Subject: [PATCH 073/376] fix: Add separate function to validate payment entry taxes --- .../doctype/payment_entry/payment_entry.py | 22 ++++++++++++-- erpnext/controllers/accounts_controller.py | 29 +++++++++---------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 70b38735e7c..3a1c7b92349 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -18,8 +18,7 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from six import string_types, iteritems -from erpnext.controllers.accounts_controller import validate_conversion_rate, \ - validate_taxes_and_charges, validate_inclusive_tax +from erpnext.controllers.accounts_controller import validate_taxes_and_charges class InvalidPaymentEntry(ValidationError): pass @@ -925,6 +924,25 @@ class PaymentEntry(AccountsController): return current_tax_fraction +def validate_inclusive_tax(tax, doc): + def _on_previous_row_error(row_range): + throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) + + if cint(getattr(tax, "included_in_paid_amount", None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + @frappe.whitelist() def get_outstanding_reference_documents(args): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a507159cb65..1cd0f5f5b27 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1211,21 +1211,20 @@ def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) - for fieldname in ['included_in_print_rate', 'included_in_paid_amount']: - if cint(getattr(tax, fieldname, None)): - if tax.charge_type == "Actual": - # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].get(fieldname)): - # referred row should also be inclusive - _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.get(fieldname) for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): - # all rows about the referred tax should be inclusive - _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) - elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + if cint(getattr(tax, "included_in_print_rate", None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.included_in_print_rate for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): From 9eac4d0af66ab1952e20d0236d675ff09f03657a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 14:44:19 +0530 Subject: [PATCH 074/376] fix: Import throw --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3a1c7b92349..b6b2bef9633 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext, json -from frappe import _, scrub, ValidationError +from frappe import _, scrub, ValidationError, throw from frappe.utils import flt, comma_or, nowdate, getdate, cint from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on from erpnext.accounts.party import get_party_account From 3b070d1b0540d320edecceefec2bf51a3e308bb2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Jun 2021 14:51:33 +0530 Subject: [PATCH 075/376] fix: Flaky test for Report Subcontracted Raw materials to be transferred --- ...tracted_raw_materials_to_be_transferred.py | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 11ec7669b0c..2448e17c50f 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -30,42 +30,54 @@ class TestSubcontractedItemToBeTransferred(unittest.TestCase): po.reload() po_data = [row for row in data if row.get('purchase_order') == po.name] + # Alphabetically sort to be certain of order + po_data = sorted(po_data, key = lambda i: i['rm_item_code']) self.assertEqual(len(po_data), 2) - self.assertIn(po_data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) - self.assertIn(po_data[0]['p_qty'], [9, 18]) - self.assertIn(po_data[0]['transferred_qty'], [1, 2]) + self.assertEqual(po_data[0]['purchase_order'], po.name) - self.assertEqual(data[1]['purchase_order'], po.name) - self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) - self.assertIn(data[1]['p_qty'], [9, 18]) - self.assertIn(data[1]['transferred_qty'], [1, 2]) + self.assertEqual(po_data[0]['rm_item_code'], '_Test Item') + self.assertEqual(po_data[0]['p_qty'], 8) + self.assertEqual(po_data[0]['transferred_qty'], 2) + + self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100') + self.assertEqual(po_data[1]['p_qty'], 19) + self.assertEqual(po_data[1]['transferred_qty'], 1) se.cancel() po.cancel() def transfer_subcontracted_raw_materials(po): + # Order of supplied items fetched in PO is flaky + transfer_qty_map = { + '_Test Item': 2, + '_Test Item Home Desktop 100': 1 + } + + item_1 = po.supplied_items[0].rm_item_code + item_2 = po.supplied_items[1].rm_item_code + rm_item = [ { 'name': po.supplied_items[0].name, - 'item_code': '_Test Item Home Desktop 100', - 'rm_item_code': '_Test Item Home Desktop 100', - 'item_name': '_Test Item Home Desktop 100', - 'qty': 2, + 'item_code': item_1, + 'rm_item_code': item_1, + 'item_name': item_1, + 'qty': transfer_qty_map[item_1], 'warehouse': '_Test Warehouse - _TC', 'rate': 100, - 'amount': 200, + 'amount': 100 * transfer_qty_map[item_1], 'stock_uom': 'Nos' }, { 'name': po.supplied_items[1].name, - 'item_code': '_Test Item', - 'rm_item_code': '_Test Item', - 'item_name': '_Test Item', - 'qty': 1, + 'item_code': item_2, + 'rm_item_code': item_2, + 'item_name': item_2, + 'qty': transfer_qty_map[item_2], 'warehouse': '_Test Warehouse - _TC', 'rate': 100, - 'amount': 100, + 'amount': 100 * transfer_qty_map[item_2], 'stock_uom': 'Nos' } ] From 27ec51f021931c6d9505e73e6852cb6c4bb97f86 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 14 Jun 2021 16:41:56 +0530 Subject: [PATCH 076/376] fix(General Ledger): Filter Cost Center drop-down list by Company --- erpnext/accounts/report/general_ledger/general_ledger.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 84f786814de..80a25c907ed 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -135,7 +135,9 @@ frappe.query_reports["General Ledger"] = { "label": __("Cost Center"), "fieldtype": "MultiSelectList", get_data: function(txt) { - return frappe.db.get_link_options('Cost Center', txt); + return frappe.db.get_link_options('Cost Center', txt, { + company: frappe.query_report.get_filter_value("company") + }); } }, { From 75b30efb050ed333d519ebad924b4cb188098521 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 14 Jun 2021 16:42:27 +0530 Subject: [PATCH 077/376] fix(General Ledger): Filter Project drop-down list by Company --- erpnext/accounts/report/general_ledger/general_ledger.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 80a25c907ed..a8e55307f9f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -145,7 +145,9 @@ frappe.query_reports["General Ledger"] = { "label": __("Project"), "fieldtype": "MultiSelectList", get_data: function(txt) { - return frappe.db.get_link_options('Project', txt); + return frappe.db.get_link_options('Project', txt, { + company: frappe.query_report.get_filter_value("company") + }); } }, { From 4cd0f6ce23f703e2f98baab83ae0b38ac9e26943 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 20:01:04 +0530 Subject: [PATCH 078/376] fix: Revert unintended changes --- erpnext/controllers/accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1cd0f5f5b27..243939b275f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1220,9 +1220,9 @@ def validate_inclusive_tax(tax, doc): # referred row should also be inclusive _on_previous_row_error(tax.row_id) elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_print_rate for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]): # all rows about the referred tax should be inclusive - _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + _on_previous_row_error("1 - %d" % (tax.row_id,)) elif tax.get("category") == "Valuation": frappe.throw(_("Valuation type charges can not be marked as Inclusive")) From d4acf87feb85bf43f4096356e6c6f8d72cb47719 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 15 Jun 2021 10:35:44 +0530 Subject: [PATCH 079/376] fix(Asset Repair): Make Accounting Dimensions section collapsible --- erpnext/assets/doctype/asset_repair/asset_repair.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 522f2874d99..b81d74bd3d0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -174,6 +174,7 @@ "fieldtype": "Section Break" }, { + "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions" @@ -239,7 +240,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-21 10:37:35.002238", + "modified": "2021-06-15 10:34:00.839353", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 285bc60684c4e03e34356e990226cbe5b00edd21 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 15 Jun 2021 12:02:07 +0530 Subject: [PATCH 080/376] fix(Asset Repair): Set asset_name as title --- erpnext/assets/doctype/asset_repair/asset_repair.json | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index b81d74bd3d0..1a714a7a3cb 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -241,6 +241,7 @@ "is_submittable": 1, "links": [], "modified": "2021-06-15 10:34:00.839353", + "title_field": "asset_name", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From b5a14911763fa29ad9f300fdd0dd1b88a2e5126f Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 15 Jun 2021 12:44:04 +0530 Subject: [PATCH 081/376] fix: escaped warehouse value for sql query (#26049) --- erpnext/controllers/stock_controller.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9c29b0076b3..6a7c9e3d0e7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -558,11 +558,8 @@ def future_sle_exists(args): or_conditions = [] for warehouse, items in warehouse_items_map.items(): or_conditions.append( - "warehouse = '{}' and item_code in ({})".format( - warehouse, - ", ".join(frappe.db.escape(item) for item in items) - ) - ) + f"""warehouse = {frappe.db.escape(warehouse)} + and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") return frappe.db.sql(""" select name From 79dc0f0afce6df44e88b223f8db7fb947047c710 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 03:37:11 +0530 Subject: [PATCH 082/376] fix: Remove console.log() --- erpnext/accounts/report/general_ledger/general_ledger.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index f3c3865b4e2..28139d14b29 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -39,7 +39,6 @@ frappe.query_reports["General Ledger"] = { "fieldtype": "MultiSelectList", "options": "Account", get_data: function(txt) { - console.log("txt = ", txt) return frappe.db.get_link_options('Account', txt, { company: frappe.query_report.get_filter_value("company") }); From 53a9ac44662be877160c5acbbafc9fcaaa16bf21 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 04:10:29 +0530 Subject: [PATCH 083/376] fix(General Ledger): Condense account_conditions --- erpnext/accounts/report/general_ledger/general_ledger.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 53c638bf4a0..aed9c4a0497 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -211,15 +211,16 @@ def get_conditions(filters): conditions = [] if filters.get("account") and not filters.get("include_dimensions"): - account_conditions = "" + account_conditions = "account in (select name from tabAccount where" for account in filters["account"]: lft, rgt = frappe.db.get_value("Account", account, ["lft", "rgt"]) - account_conditions += """account in (select name from tabAccount - where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt) + account_conditions += """ (lft>=%s and rgt<=%s) """ % (lft, rgt) # so that the OR doesn't get added to the last account condition if account != filters["account"][-1]: - account_conditions += " OR " + account_conditions += "OR" + + account_conditions += "and docstatus<2)" conditions.append(account_conditions) if filters.get("cost_center"): From 14ea9ebcb7cc34cc386c37e7f2cdc4b60a9ffd58 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 07:03:32 +0530 Subject: [PATCH 084/376] fix(Asset Repair): Display fields according to the state of the doc --- erpnext/assets/doctype/asset_repair/asset_repair.js | 2 -- erpnext/assets/doctype/asset_repair/asset_repair.json | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 7633a595a2f..fdb8d0af67f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { frappe.route_options = { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 1a714a7a3cb..c2780a13b94 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -71,6 +71,7 @@ }, { "allow_on_submit": 1, + "depends_on": "eval:!doc.__islocal", "fieldname": "completion_date", "fieldtype": "Datetime", "label": "Completion Date" @@ -78,6 +79,7 @@ { "allow_on_submit": 1, "default": "Pending", + "depends_on": "eval:!doc.__islocal", "fieldname": "repair_status", "fieldtype": "Select", "label": "Repair Status", @@ -154,6 +156,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.__islocal", "fieldname": "capitalize_repair_cost", "fieldtype": "Check", "label": "Capitalize Repair Cost" @@ -197,6 +200,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.__islocal", "fieldname": "stock_consumption", "fieldtype": "Check", "label": "Stock Consumed During Repair" @@ -231,6 +235,7 @@ "label": "Increase In Asset Life(Months)" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", @@ -240,8 +245,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-15 10:34:00.839353", - "title_field": "asset_name", + "modified": "2021-06-16 07:01:28.217619", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", @@ -280,6 +284,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "asset_name", "track_changes": 1, "track_seen": 1 } \ No newline at end of file From 695cd7099458a571f4beb086be7043a34672b8a5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 07:50:03 +0530 Subject: [PATCH 085/376] fix(Asset Repair): Add title to error messages --- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 8724d0a1254..bdc21a7b7b5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -55,9 +55,9 @@ class AssetRepair(Document): def check_for_stock_items_and_warehouse(self): if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) if not self.warehouse: - frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def check_for_cost_center(self): if not self.cost_center: From bf52f558705153098195f01b5bd786aea3337b2a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 07:56:40 +0530 Subject: [PATCH 086/376] fix(Asset Repair): Make Stock Items and Warehouse mandatory if stock_consumption is checked --- erpnext/assets/doctype/asset_repair/asset_repair.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index c2780a13b94..253321ad1ed 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -170,6 +170,7 @@ "fieldname": "stock_items", "fieldtype": "Table", "label": "Stock Items", + "mandatory_depends_on": "stock_consumption", "options": "Stock Item" }, { @@ -218,6 +219,7 @@ "label": "Total Repair Cost" }, { + "depends_on": "stock_consumption", "fieldname": "warehouse", "fieldtype": "Link", "label": "Warehouse", @@ -245,7 +247,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 07:01:28.217619", + "modified": "2021-06-16 07:52:49.438800", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 9e07b7d4a736e324ad50f7053039d10c97fabd3c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:07:06 +0530 Subject: [PATCH 087/376] fix(Asset Repair): Add Company field --- .../assets/doctype/asset_repair/asset_repair.json | 10 +++++++++- erpnext/assets/doctype/asset_repair/asset_repair.py | 12 +++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 253321ad1ed..5cc236393fe 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -11,6 +11,7 @@ "naming_series", "column_break_2", "asset_name", + "company", "section_break_5", "failure_date", "repair_status", @@ -242,12 +243,19 @@ "fieldtype": "Link", "label": "Purchase Invoice", "options": "Purchase Invoice" + }, + { + "fetch_from": "asset.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 07:52:49.438800", + "modified": "2021-06-16 08:02:34.782990", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index bdc21a7b7b5..d84810590df 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -76,7 +76,7 @@ class AssetRepair(Document): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", "stock_entry_type": "Material Issue", - "company": frappe.get_value('Asset', self.asset, "company") + "company": self.company }) for stock_item in self.stock_items: @@ -103,8 +103,7 @@ class AssetRepair(Document): def get_gl_entries(self): gl_entry = [] - company = frappe.db.get_value('Asset', self.asset, 'company') - repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') + repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') fixed_asset_account = self.get_fixed_asset_account() expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account @@ -118,7 +117,7 @@ class AssetRepair(Document): "voucher_no": self.name, "cost_center": self.cost_center, "posting_date": getdate(), - "company": company + "company": self.company }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -133,15 +132,14 @@ class AssetRepair(Document): "posting_date": getdate(), "against_voucher_type": "Purchase Invoice", "against_voucher": self.purchase_invoice, - "company": company + "company": self.company }) gl_entry.insert() def get_fixed_asset_account(self): asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) - company = frappe.db.get_value('Asset', self.asset, 'company') for account in asset_category.accounts: - if account.company_name == company: + if account.company_name == self.company: return account.fixed_asset_account def modify_depreciation_schedule(self): From 71c60f75d7f45abd16cac637ab64cc8ecd0322a8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:13:36 +0530 Subject: [PATCH 088/376] fix(Asset Repair): Filter Cost Center and Project by Company --- .../doctype/asset_repair/asset_repair.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index fdb8d0af67f..1e87722179d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -30,3 +30,21 @@ frappe.ui.form.on('Asset Repair', { } } }); + +cur_frm.fields_dict.cost_center.get_query = function(doc) { + return{ + filters:{ + 'is_group': 0, + 'company': doc.company + } + } +} + +cur_frm.fields_dict.project.get_query = function(doc) { + return{ + filters:{ + 'is_group': 0, + 'company': doc.company + } + } +} \ No newline at end of file From 94ac52c47de45273570aa09c3c56a82084ce076b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:18:45 +0530 Subject: [PATCH 089/376] fix(Asset Repair): Add mandatory_depends_on condition for Purchase Invoice --- erpnext/assets/doctype/asset_repair/asset_repair.json | 3 ++- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 5cc236393fe..b2aac7a4e61 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -242,6 +242,7 @@ "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", + "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0", "options": "Purchase Invoice" }, { @@ -255,7 +256,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 08:02:34.782990", + "modified": "2021-06-16 08:16:07.581813", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d84810590df..39f7ee2153c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -44,7 +44,6 @@ class AssetRepair(Document): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() if self.capitalize_repair_cost: - self.check_for_purchase_invoice() self.make_gl_entries() if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation'): self.modify_depreciation_schedule() @@ -89,10 +88,6 @@ class AssetRepair(Document): stock_entry.insert() stock_entry.submit() - def check_for_purchase_invoice(self): - if not self.purchase_invoice: - frappe.throw(_("Please link Purchase Invoice.")) - def on_cancel(self): self.make_gl_entries(cancel=True) From c61bbc5915c02515d053534cb831afdc4e8af13a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:26:02 +0530 Subject: [PATCH 090/376] fix(Asset Repair): Use existing function from asset.py for fetching fixed_asset_account --- erpnext/assets/doctype/asset_repair/asset_repair.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 39f7ee2153c..443c0a78f5b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, cint from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.assets.doctype.asset.asset import get_asset_account class AssetRepair(Document): def validate(self): @@ -99,7 +100,7 @@ class AssetRepair(Document): def get_gl_entries(self): gl_entry = [] repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') - fixed_asset_account = self.get_fixed_asset_account() + fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account gl_entry = frappe.get_doc({ @@ -131,12 +132,6 @@ class AssetRepair(Document): }) gl_entry.insert() - def get_fixed_asset_account(self): - asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) - for account in asset_category.accounts: - if account.company_name == self.company: - return account.fixed_asset_account - def modify_depreciation_schedule(self): if self.increase_in_asset_life: asset = frappe.get_doc('Asset', self.asset) From abb0c769a4bf1a4b7c83b78f874f47e9f501ff65 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:33:05 +0530 Subject: [PATCH 091/376] fix(Asset Repair): Uncheck allow_on_submit for all fields --- erpnext/assets/doctype/asset_repair/asset_repair.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index b2aac7a4e61..7e9587428bb 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -71,14 +71,12 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "depends_on": "eval:!doc.__islocal", "fieldname": "completion_date", "fieldtype": "Datetime", "label": "Completion Date" }, { - "allow_on_submit": 1, "default": "Pending", "depends_on": "eval:!doc.__islocal", "fieldname": "repair_status", @@ -104,13 +102,11 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "fieldname": "actions_performed", "fieldtype": "Long Text", "label": "Actions performed" }, { - "allow_on_submit": 1, "fieldname": "downtime", "fieldtype": "Data", "in_list_view": 1, @@ -122,7 +118,6 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "fieldname": "repair_cost", "fieldtype": "Currency", "label": "Repair Cost" @@ -256,7 +251,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 08:16:07.581813", + "modified": "2021-06-16 08:32:06.160615", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From a33e751b0f4dcb773586c88dfd3f025cb6a62995 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:35:50 +0530 Subject: [PATCH 092/376] fix(Asset Repair): Make Cost Center non-mandatory --- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 443c0a78f5b..688f54aee74 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -37,7 +37,6 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_for_cost_center() if self.stock_consumption or self.capitalize_repair_cost: self.increase_asset_value() @@ -59,10 +58,6 @@ class AssetRepair(Document): if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) - def check_for_cost_center(self): - if not self.cost_center: - frappe.throw(_("Please enter Cost Center.")) - def increase_asset_value(self): asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') for item in self.stock_items: From 491763fa277bb1af5241f62dedd1bd7374af7ac6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:45:54 +0530 Subject: [PATCH 093/376] fix(Asset Repair): Fix Sider issues --- .../assets/doctype/asset_repair/asset_repair.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 1e87722179d..2319b069b0c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -32,19 +32,19 @@ frappe.ui.form.on('Asset Repair', { }); cur_frm.fields_dict.cost_center.get_query = function(doc) { - return{ - filters:{ + return { + filters: { 'is_group': 0, 'company': doc.company } - } -} + }; +}; cur_frm.fields_dict.project.get_query = function(doc) { - return{ - filters:{ + return { + filters: { 'is_group': 0, 'company': doc.company } - } -} \ No newline at end of file + }; +}; \ No newline at end of file From 6d6aee29e835dfa46b85b4c3a75deb5c892b9223 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 10:42:37 +0530 Subject: [PATCH 094/376] fix(Asset Repair): Fix GL Entry creation --- .../doctype/asset_repair/asset_repair.py | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 688f54aee74..ccf8d5ca390 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -9,8 +9,9 @@ from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account +from erpnext.controllers.accounts_controller import AccountsController -class AssetRepair(Document): +class AssetRepair(AccountsController): def validate(self): if self.repair_status == "Completed" and not self.completion_date: self.completion_date = nowdate() @@ -93,39 +94,56 @@ class AssetRepair(Document): make_gl_entries(gl_entries, cancel) def get_gl_entries(self): - gl_entry = [] + gl_entries = [] repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": expense_account, - "credit": self.total_repair_cost, - "credit_in_account_currency": self.total_repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company - }) - gl_entry.insert() - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": fixed_asset_account, - "debit": self.total_repair_cost, - "debit_in_account_currency": self.total_repair_cost, - "against": expense_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice, - "company": self.company - }) - gl_entry.insert() + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) + + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "credit": self.total_repair_cost - self.repair_cost, + "credit_in_account_currency": self.total_repair_cost - self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) + + gl_entries.append( + self.get_gl_dict({ + "account": fixed_asset_account, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, + "against": expense_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "against_voucher_type": "Purchase Invoice", + "against_voucher": self.purchase_invoice, + "company": self.company + }, item=self) + ) + + return gl_entries def modify_depreciation_schedule(self): if self.increase_in_asset_life: From 34997789cdd2e328cafa1883c6a6b0fa408c5694 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 10:48:07 +0530 Subject: [PATCH 095/376] fix(Asset Repair): Filter Warehouse by Company --- erpnext/assets/doctype/asset_repair/asset_repair.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 2319b069b0c..efa6a9d4949 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -41,6 +41,14 @@ cur_frm.fields_dict.cost_center.get_query = function(doc) { }; cur_frm.fields_dict.project.get_query = function(doc) { + return { + filters: { + 'company': doc.company + } + }; +}; + +cur_frm.fields_dict.warehouse.get_query = function(doc) { return { filters: { 'is_group': 0, From 1fd80992d705f55215a89a89a90e445224937e1b Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 16 Jun 2021 13:27:34 +0530 Subject: [PATCH 096/376] fix(pos): 'NoneType' object is not iterable (#26066) --- erpnext/selling/page/point_of_sale/point_of_sale.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 7742f243852..296c8c2fd9d 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -9,7 +9,7 @@ from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability def search_by_term(search_term, warehouse, price_list): - result = search_for_serial_or_batch_or_barcode_number(search_term) + result = search_for_serial_or_batch_or_barcode_number(search_term) or {} item_code = result.get("item_code") or search_term serial_no = result.get("serial_no") or "" @@ -23,9 +23,9 @@ def search_by_term(search_term, warehouse, price_list): item_stock_qty = get_stock_availability(item_code, warehouse) price_list_rate, currency = frappe.db.get_value('Item Price', { - 'price_list': price_list, - 'item_code': item_code - }, ["price_list_rate", "currency"]) + 'price_list': price_list, + 'item_code': item_code + }, ["price_list_rate", "currency"]) or [None, None] item_info.update({ 'serial_no': serial_no, @@ -46,7 +46,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te result = [] if search_term: - result = search_by_term(search_term, warehouse, price_list) + result = search_by_term(search_term, warehouse, price_list) or [] if result: return result From b1c72da7d7511dbdf865f8014984f47c6b6d6849 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 16 Jun 2021 14:08:32 +0530 Subject: [PATCH 097/376] fix: Training event --- .../training_scheduled.json | 4 ++-- .../training_scheduled/training_scheduled.md | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json index e49541e3214..f3650038fd6 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.json +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json @@ -11,8 +11,8 @@ "event": "Submit", "idx": 0, "is_standard": 1, - "message": "\n \n \n \n \n \n \n \n
\n
\n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
\n
\n\n\n \n \n \n \n \n \n \n
\n
\n {{ doc.introduction }}\n
    \n
  • {{_(\"Event Location\")}}: {{ doc.location }}
  • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
  • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
  • \n
  • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
  • \n {% else %}\n
  • {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n
  • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n {% endif %}\n
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • \n {% if doc.is_mandatory %}\n
  • Note: This Training Event is mandatory
  • \n {% endif %}\n
\n
\n
", - "modified": "2021-05-24 16:29:13.165930", + "message": "\n \n \n \n \n \n \n \n
\n
\n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
\n
\n\n\n \n \n \n \n \n \n \n
\n
\n {{ doc.introduction }}\n
    \n
  • {{_(\"Event Location\")}}: {{ doc.location }}
  • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
  • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
  • \n
  • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
  • \n {% else %}\n
  • \n {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n
  • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}
  • \n {% endif %}\n
  • {{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • \n {% if doc.is_mandatory %}\n
  • {{ _(\"Note: This Training Event is mandatory\") }}
  • \n {% endif %}\n
\n
\n
", + "modified": "2021-06-16 14:08:12.933367", "modified_by": "Administrator", "module": "HR", "name": "Training Scheduled", diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md index 418fd4990e3..b9ba846be56 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.md +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md @@ -24,19 +24,19 @@ {% set start = frappe.utils.get_datetime(doc.start_time) %} {% set end = frappe.utils.get_datetime(doc.end_time) %} {% if start.date() == end.date() %} -
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • -
  • - {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} -
  • +
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • +
  • + {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} +
  • {% else %} -
  • {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} -
  • -
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }} -
  • +
  • + {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • +
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }}
  • {% endif %} -
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
  • {{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • {% if doc.is_mandatory %} -
  • Note: This Training Event is mandatory
  • +
  • {{ _("Note: This Training Event is mandatory") }}
  • {% endif %} @@ -44,4 +44,4 @@ - \ No newline at end of file + From 86f689e54aeafc33d2c90e5511fd04cf0458d613 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 14:23:02 +0530 Subject: [PATCH 098/376] fix(General Ledger): Get account_currency accurately --- .../report/general_ledger/general_ledger.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index aed9c4a0497..8be6aaf5642 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -91,7 +91,19 @@ def set_account_currency(filters): account_currency = None if filters.get("account"): - account_currency = get_account_currency(filters.account[0]) + if len(filters.get("account")) == 1: + account_currency = get_account_currency(filters.account[0]) + else: + currency = get_account_currency(filters.account[0]) + is_same_account_currency = True + for account in filters.get("account"): + if get_account_currency(account) != currency: + is_same_account_currency = False + break + + if is_same_account_currency: + account_currency = currency + elif filters.get("party"): gle_currency = frappe.db.get_value( "GL Entry", { From 60ce00531d0028741044bf7ed92b3feded139126 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 16 Jun 2021 14:25:55 +0530 Subject: [PATCH 099/376] fix: label for enabling ledger posting of change amount (#26070) --- .../doctype/accounts_settings/accounts_settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 2735b1cceee..0ff7230e554 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -257,9 +257,10 @@ }, { "default": "1", + "description": "If enabled, ledger entries will be posted for change amount in POS transactions", "fieldname": "post_change_gl_entries", "fieldtype": "Check", - "label": "Post Ledger Entries for Given Change" + "label": "Change Ledger Entries for Change Amount" } ], "icon": "icon-cog", @@ -267,7 +268,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-05-25 12:34:05.858669", + "modified": "2021-06-16 13:14:45.739107", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 8c73f6f19e90e1be54d17a3e1f88e7ed535bef90 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:28:26 +0530 Subject: [PATCH 100/376] fix(pos): pos loyalty card alignment (#26051) --- erpnext/public/scss/point-of-sale.scss | 10 +++++++++- erpnext/selling/page/point_of_sale/pos_payment.js | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 9bdaa8d1eeb..c77b2ce3dfa 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -806,6 +806,9 @@ display: none; float: right; font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } > .cash-shortcuts { @@ -829,6 +832,11 @@ } } } + + > .loyalty-card { + display: flex; + flex-direction: column; + } } } @@ -1134,4 +1142,4 @@ } } } -} \ No newline at end of file +} diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 156fb777fee..c484873d3e4 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -481,7 +481,7 @@ erpnext.PointOfSale.Payment = class { const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : ''; this.$payment_modes.append( `
    -
    +
    Redeem Loyalty Points
    ${amount}
    ${loyalty_program}
    @@ -563,4 +563,4 @@ erpnext.PointOfSale.Payment = class { toggle_component(show) { show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); } -}; \ No newline at end of file +}; From 3b1b4684ba37b950657fe32d44001738c4438df9 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:30:45 +0530 Subject: [PATCH 101/376] fix: check for duplicate payment terms in Payment Term Template (#26003) --- .../doctype/payment_terms_template/payment_terms_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index 80e3348d817..39627eb376a 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document): def check_duplicate_terms(self): terms = [] for term in self.terms: - term_info = (term.credit_days, term.credit_months, term.due_date_based_on) + term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) if term_info in terms: frappe.msgprint( _('The Payment Term at row {0} is possibly a duplicate.').format(term.idx), From 41b7c1aec0ae507430a6aa433c0c52b91a1ff92e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 14:40:14 +0530 Subject: [PATCH 102/376] fix(General Ledger): Improve account filter --- .../report/general_ledger/general_ledger.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 8be6aaf5642..03808c3640c 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -223,17 +223,8 @@ def get_conditions(filters): conditions = [] if filters.get("account") and not filters.get("include_dimensions"): - account_conditions = "account in (select name from tabAccount where" - for account in filters["account"]: - lft, rgt = frappe.db.get_value("Account", account, ["lft", "rgt"]) - account_conditions += """ (lft>=%s and rgt<=%s) """ % (lft, rgt) - - # so that the OR doesn't get added to the last account condition - if account != filters["account"][-1]: - account_conditions += "OR" - - account_conditions += "and docstatus<2)" - conditions.append(account_conditions) + filters.account = get_accounts_with_children(filters.account) + conditions.append("account in %(account)s") if filters.get("cost_center"): filters.cost_center = get_cost_centers_with_children(filters.cost_center) @@ -291,6 +282,20 @@ def get_conditions(filters): return "and {}".format(" and ".join(conditions)) if conditions else "" +def get_accounts_with_children(accounts): + if not isinstance(accounts, list): + accounts = [d.strip() for d in accounts.strip().split(',') if d] + + all_accounts = [] + for d in accounts: + if frappe.db.exists("Account", d): + lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"]) + children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]}) + all_accounts += [c.name for c in children] + else: + frappe.throw(_("Account: {0} does not exist").format(d)) + + return list(set(all_accounts)) def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] From 2be7e90e783d3e3c9ab5ad1f7fd034042475f2f3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 16 Jun 2021 19:00:34 +0530 Subject: [PATCH 103/376] fix(e-invoicing): IRN generation for export invoices with round off --- erpnext/regional/india/e_invoice/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 11ebef724c4..5d33c1b100a 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -385,13 +385,16 @@ def validate_totals(einvoice): if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) - if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1: + if abs( + flt(value_details['TotInvVal']) + flt(value_details['Discount']) - + flt(value_details['OthChrg']) - flt(value_details['RndOffAmt']) - + total_item_value) > 1: frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) calculated_invoice_value = \ flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ - + flt(value_details['OthChrg']) - flt(value_details['Discount']) + + flt(value_details['OthChrg']) + flt(value_details['RndOffAmt']) - flt(value_details['Discount']) if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) From 6f9de8c86fbabd0498609ed3d645c89c902c8ba3 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 16 Jun 2021 20:01:29 +0530 Subject: [PATCH 104/376] fix: removed extra space from label rate --- .../doctype/purchase_invoice_item/purchase_invoice_item.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 10e1c73ea90..8a55ff87e39 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -272,7 +272,7 @@ "fieldname": "rate", "fieldtype": "Currency", "in_list_view": 1, - "label": "Rate ", + "label": "Rate", "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency", @@ -854,7 +854,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-03-30 09:02:39.256602", + "modified": "2021-06-16 19:57:03.101571", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", From 5c19a9251f864449fed77dd403839d865f0c547d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Jun 2021 22:14:29 +0530 Subject: [PATCH 105/376] fix: Accouting Dimensions for payroll entry accrual Journal Entry --- .../doctype/payroll_entry/payroll_entry.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 7a70679db47..697d2f61670 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -11,6 +11,7 @@ from frappe import _ from erpnext.accounts.utils import get_fiscal_year from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from frappe.desk.reportview import get_match_cond, get_filters_cond +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions class PayrollEntry(Document): def onload(self): @@ -211,7 +212,7 @@ class PayrollEntry(Document): return account_dict def make_accrual_jv_entry(self): - self.check_permission('write') + self.check_permission("write") earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} payroll_payable_account = self.payroll_payable_account @@ -219,12 +220,13 @@ class PayrollEntry(Document): precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") if earnings or deductions: - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Journal Entry' - journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\ + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Journal Entry" + journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\ .format(self.start_date, self.end_date) journal_entry.company = self.company journal_entry.posting_date = self.posting_date + accounting_dimensions = get_accounting_dimensions() or [] accounts = [] currencies = [] @@ -236,37 +238,34 @@ class PayrollEntry(Document): for acc_cc, amount in earnings.items(): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount += flt(amount, precision) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": acc_cc[0], "debit_in_account_currency": flt(amt, precision), "exchange_rate": flt(exchange_rate), - "party_type": '', "cost_center": acc_cc[1] or self.cost_center, "project": self.project - }) + }, accounting_dimensions)) # Deductions for acc_cc, amount in deductions.items(): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount -= flt(amount, precision) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": acc_cc[0], "credit_in_account_currency": flt(amt, precision), "exchange_rate": flt(exchange_rate), "cost_center": acc_cc[1] or self.cost_center, - "party_type": '', "project": self.project - }) + }, accounting_dimensions)) # Payable amount exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": payroll_payable_account, "credit_in_account_currency": flt(payable_amt, precision), "exchange_rate": flt(exchange_rate), - "party_type": '', "cost_center": self.cost_center - }) + }, accounting_dimensions)) journal_entry.set("accounts", accounts) if len(currencies) > 1: @@ -286,6 +285,12 @@ class PayrollEntry(Document): return jv_name + def update_accounting_dimensions(self, row, accounting_dimensions): + for dimension in accounting_dimensions: + row.update({ dimension: self.get(dimension)}) + + return row + def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): conversion_rate = 1 exchange_rate = self.exchange_rate From 66d4e2ba51baada6ba855a04b1452d6d53f601e3 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 17 Jun 2021 08:28:19 +0530 Subject: [PATCH 106/376] fix(Asset Repair): Display value_after_depreciation in Finance Books --- .../assets/doctype/asset_finance_book/asset_finance_book.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index d9b7b695f7f..ee3a2072f0f 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -67,7 +67,6 @@ { "fieldname": "value_after_depreciation", "fieldtype": "Currency", - "hidden": 1, "label": "Value After Depreciation", "no_copy": 1, "options": "Company:company:default_currency", @@ -85,7 +84,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-05 16:30:09.213479", + "modified": "2021-06-17 08:02:32.650738", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", From 510077b3d4743297d290205c555cfeb239faa50b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Jun 2021 11:21:21 +0530 Subject: [PATCH 107/376] fix(minor): Translation and linting issues --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 697d2f61670..e71d81f323a 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -42,7 +42,7 @@ class PayrollEntry(Document): emp_with_sal_slip.append(employee_details.employee) if len(emp_with_sal_slip): - frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip))) + frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` @@ -287,7 +287,7 @@ class PayrollEntry(Document): def update_accounting_dimensions(self, row, accounting_dimensions): for dimension in accounting_dimensions: - row.update({ dimension: self.get(dimension)}) + row.update({dimension: self.get(dimension)}) return row From 05c70ac5849b15d953974f751c6adebdf6bfb51c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 17 Jun 2021 13:02:02 +0530 Subject: [PATCH 108/376] fix(Asset): Replace asset_value with value_after_depreciation in Finance Books --- erpnext/assets/doctype/asset/asset.json | 9 +-------- erpnext/assets/doctype/asset/asset.py | 11 ++++++----- .../asset_finance_book/asset_finance_book.json | 2 +- .../assets/doctype/asset_repair/asset_repair.py | 16 +++++++++++----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 8a0e3ad2a66..d55258c8f67 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -23,7 +23,6 @@ "asset_name", "asset_category", "location", - "asset_value", "custodian", "department", "disposal_date", @@ -484,12 +483,6 @@ "fieldtype": "Section Break", "label": "Finance Books" }, - { - "fieldname": "asset_value", - "fieldtype": "Currency", - "label": "Asset Value", - "read_only": 1 - }, { "fieldname": "to_date", "fieldtype": "Date", @@ -523,7 +516,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-05-21 12:05:29.424083", + "modified": "2021-06-17 12:59:39.189106", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ade74e60554..f67266bb921 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -96,9 +96,6 @@ class Asset(AccountsController): finance_books = get_item_details(self.item_code, self.asset_category) self.set('finance_books', finance_books) - if not(self.asset_value): - self.asset_value = self.gross_purchase_amount - def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -187,8 +184,12 @@ class Asset(AccountsController): start = n break - value_after_depreciation = (flt(self.asset_value) - - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + if d.value_after_depreciation: + value_after_depreciation = (flt(d.value_after_depreciation) - + flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + else: + value_after_depreciation = (flt(self.gross_purchase_amount) - + flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) d.value_after_depreciation = value_after_depreciation diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index ee3a2072f0f..e5a5f194c1b 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -84,7 +84,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-17 08:02:32.650738", + "modified": "2021-06-17 12:59:05.743683", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ccf8d5ca390..678a47e8c7a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -60,13 +60,19 @@ class AssetRepair(AccountsController): frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def increase_asset_value(self): - asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + total_value_of_stock_consumed = 0 for item in self.stock_items: - asset_value += item.total_value + total_value_of_stock_consumed += item.total_value - if self.capitalize_repair_cost: - asset_value += self.repair_cost - frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + asset = frappe.get_doc('Asset', self.asset) + asset.flags.ignore_validate_update_after_submit = True + if asset.calculate_depreciation: + for row in asset.finance_books: + row.value_after_depreciation += total_value_of_stock_consumed + + if self.capitalize_repair_cost: + row.value_after_depreciation += self.repair_cost + asset.save() def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ From 61675f1a43b086a586c8e574c3a0ac848fee2076 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Jun 2021 13:05:43 +0530 Subject: [PATCH 109/376] fix: purchase receipt gl entries with same item code --- erpnext/accounts/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 968ab49b312..6217ec5beb9 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None): def check_if_in_list(gle, gl_map, dimensions=None): account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type', - 'cost_center', 'project'] + 'cost_center', 'project', 'voucher_detail_no'] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions From f9390f596d41004ed7645b967f29fdd9df08e412 Mon Sep 17 00:00:00 2001 From: Alan <2.alan.tom@gmail.com> Date: Thu, 17 Jun 2021 18:13:23 +0530 Subject: [PATCH 110/376] fix: auto unlink warehouse from item on delete (#26073) * fix: auto unlink warehouse from item on delete * fix: sider * refactor: use delete_doc * test: add test for unlinking warehouse from item * refactor: add msgprint to inform user of unlink * refactor: cleanup and reuse extant functions * fix: don't delete row, update table --- .../stock/doctype/warehouse/test_warehouse.py | 34 +++++++++++++++++++ erpnext/stock/doctype/warehouse/warehouse.py | 7 ++++ 2 files changed, 41 insertions(+) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 95478f61f0a..e3981c913e1 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -11,6 +11,7 @@ from frappe.test_runner import make_test_records import erpnext from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account +from erpnext.stock.doctype.item.test_item import create_item test_records = frappe.get_test_records('Warehouse') @@ -92,6 +93,39 @@ class TestWarehouse(unittest.TestCase): self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": "Test Warehouse for Merging 2 - TCP1"})) + def test_unlinking_warehouse_from_item_defaults(self): + company = "_Test Company" + + warehouse_names = [f'_Test Warehouse {i} for Unlinking' for i in range(2)] + warehouse_ids = [] + for warehouse in warehouse_names: + warehouse_id = create_warehouse(warehouse, company=company) + warehouse_ids.append(warehouse_id) + + item_names = [f'_Test Item {i} for Unlinking' for i in range(2)] + for item, warehouse in zip(item_names, warehouse_ids): + create_item(item, warehouse=warehouse, company=company) + + # Delete warehouses + for warehouse in warehouse_ids: + frappe.delete_doc("Warehouse", warehouse) + + # Check Item existance + for item in item_names: + self.assertTrue( + bool(frappe.db.exists("Item", item)), + f"{item} doesn't exist" + ) + + item_doc = frappe.get_doc("Item", item) + for item_default in item_doc.item_defaults: + self.assertNotIn( + item_default.default_warehouse, + warehouse_ids, + f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}." + ) + + def create_warehouse(warehouse_name, properties=None, company=None): if not company: company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 2062bddc7c9..3abc13907cf 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -54,6 +54,7 @@ class Warehouse(NestedSet): throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse.")) self.update_nsm_model() + self.unlink_from_items() def check_if_sle_exists(self): return frappe.db.sql("""select name from `tabStock Ledger Entry` @@ -138,6 +139,12 @@ class Warehouse(NestedSet): self.save() return 1 + def unlink_from_items(self): + frappe.db.sql(""" + update `tabItem Default` + set default_warehouse=NULL + where default_warehouse=%s""", self.name) + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if is_root: From ddef85ae97024376915b94d52ad347ba2b9c9c8c Mon Sep 17 00:00:00 2001 From: Eben van Deventer Date: Thu, 17 Jun 2021 15:13:30 +0200 Subject: [PATCH 111/376] fix: Correct South Africa VAT Rate (#25894) On 1 April 2018 South Africa increased the VAT rate from 14% to 15%, this proposed change seeks to update the default parameters for a fresh ERPNext installation. --- erpnext/setup/setup_wizard/data/country_wise_tax.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index ec9a6d6b709..daaa626a81a 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1867,7 +1867,7 @@ "South Africa": { "South Africa Tax": { "account_name": "VAT", - "tax_rate": 14.00 + "tax_rate": 15.00 } }, From 59e2e4989b4cdb88d7812161837924ba00a3029d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Jun 2021 19:58:16 +0530 Subject: [PATCH 112/376] fix: Incorrect billed qty in Sales Order analytics --- .../selling/report/sales_order_analysis/sales_order_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index f5feb95f1a8..8cb24460f7b 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -59,7 +59,7 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, - IFNULL(sii.qty, 0) as billed_qty, + IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, From 354116142a856c81750942ee6f887c3d529b1731 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 18 Jun 2021 09:53:18 +0530 Subject: [PATCH 113/376] fix(Asset Repair): Create GL Entries for each item in Stock Items --- .../doctype/asset_repair/asset_repair.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 678a47e8c7a..3bcf958db60 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -119,19 +119,23 @@ class AssetRepair(AccountsController): }, item=self) ) - gl_entries.append( - self.get_gl_dict({ - "account": expense_account, - "credit": self.total_repair_cost - self.repair_cost, - "credit_in_account_currency": self.total_repair_cost - self.repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company - }, item=self) - ) + if self.stock_consumption: + # creating GL Entries for each row in Stock Items based on the Stock Entry created for it + stock_entry = frappe.get_last_doc('Stock Entry') + for item in stock_entry.items: + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "credit": item.amount, + "credit_in_account_currency": item.amount, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) gl_entries.append( self.get_gl_dict({ From 94dfd0e318cf13164a2b6eab5ed7d9cef369467f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 18 Jun 2021 09:59:45 +0530 Subject: [PATCH 114/376] fix(Asset): Add function to clear old depreciation schedule --- erpnext/assets/doctype/asset/asset.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f67266bb921..1cef290a7af 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -176,13 +176,8 @@ class Asset(AccountsController): for d in self.get('finance_books'): self.validate_asset_finance_books(d) - - start = 0 - for n in range(len(self.schedules)): - if not self.schedules[n].journal_entry: - del self.schedules[n:] - start = n - break + + start = self.clear_depreciation_schedule() if d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - @@ -296,6 +291,15 @@ class Asset(AccountsController): "finance_book_id": d.idx }) + def clear_depreciation_schedule(self): + start = 0 + for n in range(len(self.schedules)): + if not self.schedules[n].journal_entry: + del self.schedules[n:] + start = n + break + return start + def check_is_pro_rata(self, row): has_pro_rata = False From ef972693861bb2b39071351249292fe1ab136432 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 18 Jun 2021 10:11:53 +0530 Subject: [PATCH 115/376] fix: timeout while cancelling stock reconciliation --- .../doctype/stock_reconciliation/stock_reconciliation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 306df99b3c6..2b51c1a5c38 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -473,6 +473,13 @@ class StockReconciliation(StockController): else: self._submit() + def cancel(self): + if len(self.items) > 100: + msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage")) + self.queue_action('cancel', timeout=2000) + else: + self._cancel() + @frappe.whitelist() def get_items(warehouse, posting_date, posting_time, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) From a58b571ccb727ffadded90c1b8b7541bd8c6c8c4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Jun 2021 10:45:35 +0530 Subject: [PATCH 116/376] fix: Billing address not fetched in Purchase Invoice --- .../doctype/purchase_invoice/purchase_invoice.js | 10 +++++----- erpnext/public/js/controllers/transaction.js | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index f58c8f45262..dc9094c3e91 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }); }, - company: function() { - erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - }, - onload: function() { this._super(); @@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", { frm: frm, freeze_message: __("Creating Purchase Receipt ...") }) - } + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, }) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 89fed3bf0dc..340c0933ef9 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -864,9 +864,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } - if (this.frm.doc.posting_date) var date = this.frm.doc.posting_date; - else var date = this.frm.doc.transaction_date; - if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ From b066fe9519726adc26cf7a0a065f4504d8cc6e1d Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:29:07 +0530 Subject: [PATCH 117/376] fix: insufficient permission for dunning error (#26092) --- erpnext/accounts/doctype/dunning/dunning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 1a6dbedf560..c6c689212b6 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -86,7 +86,7 @@ def resolve_dunning(doc, state): for reference in doc.references: if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: dunnings = frappe.get_list('Dunning', filters={ - 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}) + 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True) for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') @@ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte grand_total = 0 if rate_of_interest: interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 - interest_amount = (interest_per_year * cint(overdue_days)) / 365 + interest_amount = (interest_per_year * cint(overdue_days)) / 365 grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { From 3d8f82459b0bd90c80aec473f9a0daa5a7564db8 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Jun 2021 11:42:28 +0530 Subject: [PATCH 118/376] fix(Issue): reset response_by and resolution_by if SLA is removed (#25997) --- erpnext/support/doctype/issue/issue.json | 6 +++--- erpnext/support/doctype/issue/issue.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index bc29821ee24..14712f89feb 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -166,7 +166,7 @@ "options": "Service Level Agreement" }, { - "depends_on": "eval: doc.status != 'Replied';", + "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;", "fieldname": "response_by", "fieldtype": "Datetime", "label": "Response By", @@ -180,7 +180,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.status != 'Replied';", + "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;", "fieldname": "resolution_by", "fieldtype": "Datetime", "label": "Resolution By", @@ -410,7 +410,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2021-05-26 10:49:07.574769", + "modified": "2021-06-10 03:22:27.098898", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index b068363f061..9c69deb6a48 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -29,6 +29,9 @@ class Issue(Document): self.update_status() self.set_lead_contact(self.raised_by) + if not self.service_level_agreement: + self.reset_sla_fields() + def on_update(self): # Add a communication in the issue timeline if self.flags.create_communication and self.via_customer_portal: @@ -54,6 +57,13 @@ class Issue(Document): self.company = frappe.db.get_value("Lead", self.lead, "company") or \ frappe.db.get_default("Company") + def reset_sla_fields(self): + self.agreement_status = "" + self.response_by = "" + self.resolution_by = "" + self.response_by_variance = 0 + self.resolution_by_variance = 0 + def update_status(self): status = frappe.db.get_value("Issue", self.name, "status") if self.status != "Open" and status == "Open" and not self.first_responded_on: @@ -511,4 +521,4 @@ def get_time_in_timedelta(time): Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215) """ import datetime - return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) \ No newline at end of file + return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) From 81c97c13ce1b697acc16d38f1e1084f5da573882 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Jun 2021 17:25:37 +0530 Subject: [PATCH 119/376] fix: Sanctioned loan amount limit check --- erpnext/loan_management/doctype/loan/loan.py | 29 +++++- .../loan_management/doctype/loan/test_loan.py | 45 ++++++++- .../loan_application/loan_application.py | 4 +- .../doctype/loan_repayment/loan_repayment.py | 91 ++++++++++--------- 4 files changed, 116 insertions(+), 53 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 69d11a8653e..ff7fbbdf49a 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -60,8 +60,9 @@ class Loan(AccountsController): self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) def check_sanctioned_amount_limit(self): - total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit: + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) @@ -155,9 +156,29 @@ def update_total_amount_paid(doc): frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) def get_total_loan_amount(applicant_type, applicant, company): - return frappe.db.get_value('Loan', - {'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, - 'sum(loan_amount)') + pending_amount = 0 + loan_details = frappe.db.get_all("Loan", + filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1, + "status": ("!=", "Closed")}, + fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid", + "written_off_amount"]) + + interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type, + "company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)")) + + for loan in loan_details: + if loan.status in ("Disbursed", "Loan Closure Requested"): + pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + elif loan.status == "Partially Disbursed": + pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + elif loan.status == "Sanctioned": + pending_amount += flt(loan.total_payment) + + pending_amount += interest_amount + + return pending_amount def get_sanctioned_amount_limit(applicant_type, applicant, company): return frappe.db.get_value('Sanctioned Loan Amount', diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index fa4707ce2b4..314f58dd15e 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase): if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) - self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + if not frappe.db.exists("Customer", "_Test Loan Customer 1"): + frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True) + + self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") + self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name") create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) @@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase): self.assertTrue(gl_entries1) self.assertTrue(gl_entries2) + def test_sanctioned_amount_limit(self): + # Clear loan docs before checking + frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'") + frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'") + frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'") + + if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", "company": "_Test Company"}): + frappe.get_doc({ + "doctype": "Sanctioned Loan Amount", + "applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", + "sanctioned_amount_limit": 1500000, + "company": "_Test Company" + }).insert(ignore_permissions=True) + + # Make First Loan + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge) + create_pledge(loan_application) + loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + # Make second loan greater than the sanctioned amount + loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge, + do_not_save=True) + self.assertRaises(frappe.ValidationError, loan_application.save) + def test_regular_loan_repayment(self): pledge = [{ "loan_security": "Test Security 1", @@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase): unpledge_request.load_from_db() self.assertEqual(unpledge_request.docstatus, 1) - def test_santined_loan_security_unpledge(self): + def test_sanctioned_loan_security_unpledge(self): pledge = [{ "loan_security": "Test Security 1", "qty": 4000.00 @@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount): return lr def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, - repayment_periods=None, posting_date=None): + repayment_periods=None, posting_date=None, do_not_save=False): loan_application = frappe.new_doc('Loan Application') loan_application.applicant_type = 'Customer' loan_application.company = company @@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep for pledge in proposed_pledges: loan_application.append('proposed_pledges', pledge) + if do_not_save: + return loan_application + loan_application.save() loan_application.submit() diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index 9c0147e55ba..d8f3577b2c3 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -46,9 +46,11 @@ class LoanApplication(Document): frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) def check_sanctioned_amount_limit(self): - total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit: + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 3d99b1f3040..b8b1a40b5fd 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -235,70 +235,71 @@ class LoanRepayment(AccountsController): else: remarks = _("Repayment against Loan: ") + self.against_loan - if self.total_penalty_paid: + if not loan_details.repay_from_salary: + if self.total_penalty_paid: + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "against": loan_details.payment_account, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.penalty_income_account, + "against": loan_details.payment_account, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, - "debit": self.total_penalty_paid, - "debit_in_account_currency": self.total_penalty_paid, + "account": loan_details.payment_account, + "against": loan_details.loan_account + ", " + loan_details.interest_income_account + + ", " + loan_details.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) gle_map.append( self.get_gl_dict({ - "account": loan_details.penalty_income_account, + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, "against": loan_details.payment_account, - "credit": self.total_penalty_paid, - "credit_in_account_currency": self.total_penalty_paid, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) ) - gle_map.append( - self.get_gl_dict({ - "account": loan_details.payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, - "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, - "against": loan_details.payment_account, - "credit": self.amount_paid, - "credit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - if gle_map: - make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) def create_repayment_entry(loan, applicant, company, posting_date, loan_type, payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): From 8520edc952d252aa4ebdbf9bf56e2b04a12dd614 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 15 Jun 2021 10:21:44 +0530 Subject: [PATCH 120/376] fix: time out while submitting the stock transactions with more than 50 items --- erpnext/controllers/buying_controller.py | 10 ++- erpnext/controllers/stock_controller.py | 79 ++++++++++++++----- .../stock_ledger_entry/stock_ledger_entry.py | 19 ++--- erpnext/stock/stock_ledger.py | 16 +++- erpnext/stock/utils.py | 2 +- 5 files changed, 93 insertions(+), 33 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index da819119b10..20f54457259 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -171,12 +171,13 @@ class BuyingController(StockController): TODO: rename item_tax_amount to valuation_tax_amount """ + stock_and_asset_items = [] stock_and_asset_items = self.get_stock_items() + self.get_asset_items() stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 last_item_idx = 1 for d in self.get("items"): - if d.item_code and d.item_code in stock_and_asset_items: + if (d.item_code and d.item_code in stock_and_asset_items): stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_amount += flt(d.base_net_amount) last_item_idx = d.idx @@ -683,7 +684,8 @@ class BuyingController(StockController): self.process_fixed_asset() self.update_fixed_asset(field) - update_last_purchase_rate(self, is_submit = 1) + if self.doctype in ['Purchase Order', 'Purchase Receipt']: + update_last_purchase_rate(self, is_submit = 1) def on_cancel(self): super(BuyingController, self).on_cancel() @@ -691,7 +693,9 @@ class BuyingController(StockController): if self.get('is_return'): return - update_last_purchase_rate(self, is_submit = 0) + if self.doctype in ['Purchase Order', 'Purchase Receipt']: + update_last_purchase_rate(self, is_submit = 0) + if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 6a7c9e3d0e7..35097b97b99 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -501,7 +501,6 @@ class StockController(AccountsController): check_if_stock_and_account_balance_synced(self.posting_date, self.company, self.doctype, self.name) - @frappe.whitelist() def make_quality_inspections(doctype, docname, items): if isinstance(items, str): @@ -533,21 +532,75 @@ def make_quality_inspections(doctype, docname, items): return inspections - def is_reposting_pending(): return frappe.db.exists("Repost Item Valuation", {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) +def future_sle_exists(args, sl_entries=None): + key = (args.voucher_type, args.voucher_no) -def future_sle_exists(args): - sl_entries = frappe.get_all("Stock Ledger Entry", + if validate_future_sle_not_exists(args, key, sl_entries): + return False + elif get_cached_data(args, key): + return True + + if not sl_entries: + sl_entries = get_sle_entries_against_voucher(args) + if not sl_entries: + return + + or_conditions = get_conditions_to_validate_future_sle(sl_entries) + + data = frappe.db.sql(""" + select item_code, warehouse, count(name) as total_row + from `tabStock Ledger Entry` + where + ({}) + and timestamp(posting_date, posting_time) + >= timestamp(%(posting_date)s, %(posting_time)s) + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + GROUP BY + item_code, warehouse + """.format(" or ".join(or_conditions)), args, as_dict=1) + + for d in data: + frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row + + return len(data) + +def validate_future_sle_not_exists(args, key, sl_entries=None): + item_key = '' + if args.get('item_code'): + item_key = (args.get('item_code'), args.get('warehouse')) + + if not sl_entries and hasattr(frappe.local, 'future_sle'): + if (not frappe.local.future_sle.get(key) or + (item_key and item_key not in frappe.local.future_sle.get(key))): + return True + +def get_cached_data(args, key): + if not hasattr(frappe.local, 'future_sle'): + frappe.local.future_sle = {} + + if key not in frappe.local.future_sle: + frappe.local.future_sle[key] = frappe._dict({}) + + if args.get('item_code'): + item_key = (args.get('item_code'), args.get('warehouse')) + count = frappe.local.future_sle[key].get(item_key) + + return True if (count or count == 0) else False + else: + return frappe.local.future_sle[key] + +def get_sle_entries_against_voucher(args): + return frappe.get_all("Stock Ledger Entry", filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, fields=["item_code", "warehouse"], order_by="creation asc") - if not sl_entries: - return - +def get_conditions_to_validate_future_sle(sl_entries): warehouse_items_map = {} for entry in sl_entries: if entry.warehouse not in warehouse_items_map: @@ -561,17 +614,7 @@ def future_sle_exists(args): f"""warehouse = {frappe.db.escape(warehouse)} and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") - return frappe.db.sql(""" - select name - from `tabStock Ledger Entry` - where - ({}) - and timestamp(posting_date, posting_time) - >= timestamp(%(posting_date)s, %(posting_time)s) - and voucher_no != %(voucher_no)s - and is_cancelled = 0 - limit 1 - """.format(" or ".join(or_conditions)), args) + return or_conditions def create_repost_item_valuation_entry(args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index b0e7440e6cc..0febcb68910 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, date_diff +from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, cint from frappe.model.document import Document from datetime import date from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock @@ -108,17 +108,18 @@ class StockLedgerEntry(Document): self.stock_uom = item_det.stock_uom def check_stock_frozen_date(self): - stock_frozen_upto = frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto') or '' - if stock_frozen_upto: - stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role') - if getdate(self.posting_date) <= getdate(stock_frozen_upto) and not stock_auth_role in frappe.get_roles(): - frappe.throw(_("Stock transactions before {0} are frozen").format(formatdate(stock_frozen_upto)), StockFreezeError) + stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings') - stock_frozen_upto_days = int(frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto_days') or 0) + if stock_settings.stock_frozen_upto: + if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) + and stock_settings.stock_auth_role not in frappe.get_roles()): + frappe.throw(_("Stock transactions before {0} are frozen") + .format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError) + + stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days) if stock_frozen_upto_days: - stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role') older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()) - if older_than_x_days_ago and not stock_auth_role in frappe.get_roles(): + if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles(): frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError) def scrub_posting_time(self): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fc82c789cc7..fb2ecab2491 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -22,6 +22,7 @@ _exceptions = frappe.local('stockledger_exceptions') # _exceptions = [] def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): + from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: from erpnext.stock.utils import update_bin @@ -30,6 +31,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc validate_cancellation(sl_entries) set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) + args = get_args_for_future_sle(sl_entries[0]) + future_sle_exists(args, sl_entries) + for sle in sl_entries: if sle.serial_no: validate_serial_no(sle) @@ -53,6 +57,14 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc args = sle_doc.as_dict() update_bin(args, allow_negative_stock, via_landed_cost_voucher) +def get_args_for_future_sle(row): + return frappe._dict({ + 'voucher_type': row.get('voucher_type'), + 'voucher_no': row.get('voucher_no'), + 'posting_date': row.get('posting_date'), + 'posting_time': row.get('posting_time') + }) + def validate_serial_no(sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for sn in get_serial_nos(sle.serial_no): @@ -472,8 +484,8 @@ class update_entries_after(object): frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate) # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice - if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): - doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': + doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) for d in (doc.items + doc.supplied_items): d.db_update() diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 034d3ebbb54..8a6a3a3e4a0 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -177,7 +177,7 @@ def get_bin(item_code, warehouse): return bin_obj def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): - is_stock_item = frappe.db.get_value('Item', args.get("item_code"), 'is_stock_item') + is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: bin = get_bin(args.get("item_code"), args.get("warehouse")) bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher) From ba288274f21f057765500e62f9ffbaf718913aef Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 19 Jun 2021 12:31:12 +0530 Subject: [PATCH 121/376] fix: ignore permission to update call log --- erpnext/telephony/doctype/call_log/call_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 4d553df08b8..c00dfa90566 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -142,7 +142,7 @@ def link_existing_conversations(doc, state): for log in logs: call_log = frappe.get_doc('Call Log', log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) - call_log.save() + call_log.save(ignore_permissions=True) frappe.db.commit() except Exception: frappe.log_error(title=_('Error during caller information update')) From 93b975277179481b9884215d6d15a44d76fdd3d2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 13:06:27 +0530 Subject: [PATCH 122/376] fix: Add comments --- erpnext/assets/doctype/asset/asset.py | 12 ++++++-- .../doctype/asset_repair/asset_repair.py | 30 +++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 1cef290a7af..9a8b6c97913 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -179,7 +179,8 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() - if d.value_after_depreciation: + # value_after_depreciation - current Asset value + if d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) else: @@ -291,6 +292,7 @@ class Asset(AccountsController): "finance_book_id": d.idx }) + # used when depreciation schedule needs to be modified due to increase in asset life def clear_depreciation_schedule(self): start = 0 for n in range(len(self.schedules)): @@ -300,10 +302,13 @@ class Asset(AccountsController): break return start + + # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False - days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + + # if frequency_of_depreciation is 12 months, total_days = 365 total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) if days < total_days: @@ -783,9 +788,12 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) if row.depreciation_method in ("Straight Line", "Manual"): + # if the Depreciation Schedule is being prepared for the first time if not asset.to_date: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left + + # if the Depreciation Schedule is being modified after Asset Repair else: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3bcf958db60..fb815a24516 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -46,7 +46,7 @@ class AssetRepair(AccountsController): self.decrease_stock_quantity() if self.capitalize_repair_cost: self.make_gl_entries() - if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation'): + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: self.modify_depreciation_schedule() def check_repair_status(self): @@ -156,27 +156,33 @@ class AssetRepair(AccountsController): return gl_entries def modify_depreciation_schedule(self): - if self.increase_in_asset_life: - asset = frappe.get_doc('Asset', self.asset) - asset.flags.ignore_validate_update_after_submit = True - for row in asset.finance_books: - row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + asset = frappe.get_doc('Asset', self.asset) + asset.flags.ignore_validate_update_after_submit = True + for row in asset.finance_books: + row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation - asset.edit_dates = "" - extra_months = self.increase_in_asset_life % row.frequency_of_depreciation - if extra_months != 0: - self.calculate_last_schedule_date(asset, row, extra_months) + asset.edit_dates = "" + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date(asset, row, extra_months) - asset.prepare_depreciation_data() - asset.save() + asset.prepare_depreciation_data() + asset.save() # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation def calculate_last_schedule_date(self, asset, row, extra_months): asset.edit_dates = "Don't Edit" number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ cint(asset.number_of_depreciations_booked) + + # the Schedule Date in the final row of the old Depreciation Schedule last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + + # the Schedule Date in the final row of the new Depreciation Schedule asset.to_date = add_months(last_schedule_date, extra_months) + + # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations + # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... schedule_date = add_months(row.depreciation_start_date, number_of_pending_depreciations * cint(row.frequency_of_depreciation)) From 2b93e54e1f43e35d2b51ea3b954ef7ad87892844 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 13:45:37 +0530 Subject: [PATCH 123/376] fix(Asset Repair): Fix depreciation_amount calculation --- erpnext/assets/doctype/asset/asset.py | 2 +- erpnext/regional/india/utils.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9a8b6c97913..a2917fe3998 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -789,7 +789,7 @@ def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time - if not asset.to_date: + if not asset.edit_dates: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 075c698fead..4d373444a6f 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -838,8 +838,16 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) if row.depreciation_method in ("Straight Line", "Manual"): - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left + # if the Depreciation Schedule is being prepared for the first time + if not asset.edit_dates: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / depreciation_left + + # if the Depreciation Schedule is being modified after Asset Repair + else: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + else: rate_of_depreciation = row.rate_of_depreciation # if its the first depreciation From da8da9fa4e2991029d7c90e9fdd8243360b9f3f4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 14:00:26 +0530 Subject: [PATCH 124/376] fix: Replace edit_dates with flags.increase_in_asset_life --- erpnext/assets/doctype/asset/asset.json | 9 +-------- erpnext/assets/doctype/asset/asset.py | 4 ++-- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++-- erpnext/regional/india/utils.py | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index d55258c8f67..d77eb104184 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -54,7 +54,6 @@ "section_break_14", "schedules", "to_date", - "edit_dates", "insurance_details", "policy_number", "insurer", @@ -488,12 +487,6 @@ "fieldtype": "Date", "hidden": 1, "label": "To Date" - }, - { - "fieldname": "edit_dates", - "fieldtype": "Data", - "hidden": 1, - "label": "Edit Dates" } ], "idx": 72, @@ -516,7 +509,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-06-17 12:59:39.189106", + "modified": "2021-06-19 13:56:58.450182", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a2917fe3998..27d21e25420 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -223,7 +223,7 @@ class Asset(AccountsController): # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - if not self.edit_dates: + if not self.flags.increase_in_asset_life: self.to_date = add_months(self.available_for_use_date, n * cint(d.frequency_of_depreciation)) @@ -789,7 +789,7 @@ def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time - if not asset.edit_dates: + if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index fb815a24516..9ef6591499c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -161,7 +161,7 @@ class AssetRepair(AccountsController): for row in asset.finance_books: row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation - asset.edit_dates = "" + asset.flags.increase_in_asset_life = False extra_months = self.increase_in_asset_life % row.frequency_of_depreciation if extra_months != 0: self.calculate_last_schedule_date(asset, row, extra_months) @@ -171,7 +171,7 @@ class AssetRepair(AccountsController): # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation def calculate_last_schedule_date(self, asset, row, extra_months): - asset.edit_dates = "Don't Edit" + asset.flags.increase_in_asset_life = True number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ cint(asset.number_of_depreciations_booked) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 4d373444a6f..0dafe01711a 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -839,7 +839,7 @@ def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time - if not asset.edit_dates: + if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left From 09ba6f64770102520bb5c94f7ce862f9f9752597 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 14:06:45 +0530 Subject: [PATCH 125/376] fix(Asset Repair): Move Total Repair Cost to the Stock Consumption Details section --- erpnext/assets/doctype/asset_repair/asset_repair.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 7e9587428bb..f43b5d92bf8 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -26,11 +26,11 @@ "capitalize_repair_cost", "stock_consumption", "column_break_8", - "total_repair_cost", "purchase_invoice", "stock_consumption_details_section", "warehouse", "stock_items", + "total_repair_cost", "asset_depreciation_details_section", "increase_in_asset_life", "section_break_9", @@ -210,6 +210,7 @@ }, { "depends_on": "stock_consumption", + "description": "Sum of Repair Cost and the total value of all Stock Items consumed during the repair.", "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost" @@ -251,7 +252,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 08:32:06.160615", + "modified": "2021-06-19 14:04:35.423111", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 7db7988e4c08db88c1a9872db29748afa10a1a67 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 14:54:30 +0530 Subject: [PATCH 126/376] fix(Asset Repair): Add Stock Entry field --- .../doctype/asset_repair/asset_repair.json | 16 +++++++++++++--- .../assets/doctype/asset_repair/asset_repair.py | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index f43b5d92bf8..6c90ca23480 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -31,6 +31,7 @@ "warehouse", "stock_items", "total_repair_cost", + "stock_entry", "asset_depreciation_details_section", "increase_in_asset_life", "section_break_9", @@ -118,6 +119,7 @@ "fieldtype": "Column Break" }, { + "default": "0", "fieldname": "repair_cost", "fieldtype": "Currency", "label": "Repair Cost" @@ -209,11 +211,12 @@ "label": "Stock Consumption Details" }, { - "depends_on": "stock_consumption", + "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0", "description": "Sum of Repair Cost and the total value of all Stock Items consumed during the repair.", "fieldname": "total_repair_cost", "fieldtype": "Currency", - "label": "Total Repair Cost" + "label": "Total Repair Cost", + "read_only": 1 }, { "depends_on": "stock_consumption", @@ -247,12 +250,19 @@ "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "stock_entry", + "fieldtype": "Link", + "label": "Stock Entry", + "options": "Stock Entry", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 14:04:35.423111", + "modified": "2021-06-19 14:47:25.875814", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9ef6591499c..212af7a930b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -91,6 +91,8 @@ class AssetRepair(AccountsController): stock_entry.insert() stock_entry.submit() + self.stock_entry = stock_entry.name + def on_cancel(self): self.make_gl_entries(cancel=True) @@ -121,7 +123,7 @@ class AssetRepair(AccountsController): if self.stock_consumption: # creating GL Entries for each row in Stock Items based on the Stock Entry created for it - stock_entry = frappe.get_last_doc('Stock Entry') + stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) for item in stock_entry.items: gl_entries.append( self.get_gl_dict({ From 012b9eaeff8a2729ec1962260f5c05d8fb2fc5c3 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 15:18:54 +0530 Subject: [PATCH 127/376] fix(Asset Repair): Make Error Description non-mandatory --- erpnext/assets/doctype/asset_repair/asset_repair.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 6c90ca23480..53d72ab68f8 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -95,8 +95,7 @@ { "fieldname": "description", "fieldtype": "Long Text", - "label": "Error Description", - "reqd": 1 + "label": "Error Description" }, { "fieldname": "column_break_9", @@ -262,7 +261,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 14:47:25.875814", + "modified": "2021-06-19 15:18:10.625833", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 23876c085407866ca40e2248d9a6636d5cdff6da Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 15:23:06 +0530 Subject: [PATCH 128/376] fix(Asset Repair): Prevent some fields from being copied on duplicating the doc --- erpnext/assets/doctype/asset_repair/asset_repair.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 53d72ab68f8..6f9b6863f7e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -75,7 +75,8 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "completion_date", "fieldtype": "Datetime", - "label": "Completion Date" + "label": "Completion Date", + "no_copy": 1 }, { "default": "Pending", @@ -233,7 +234,8 @@ { "fieldname": "increase_in_asset_life", "fieldtype": "Int", - "label": "Increase In Asset Life(Months)" + "label": "Increase In Asset Life(Months)", + "no_copy": 1 }, { "depends_on": "eval:!doc.__islocal", @@ -241,6 +243,7 @@ "fieldtype": "Link", "label": "Purchase Invoice", "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0", + "no_copy": 1, "options": "Purchase Invoice" }, { @@ -261,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 15:18:10.625833", + "modified": "2021-06-19 15:20:24.056706", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 8c844e4515afc5f9f241c522578b2e81d19f24b9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 11 Jun 2021 17:27:08 +0530 Subject: [PATCH 129/376] fix: material request and supplier quotation not linked if sq created from supplier portal against rfq --- .../request_for_quotation.py | 22 ++++++++++--------- .../templates/includes/transaction_row.html | 8 ++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 0127eb81630..a4ce84e1cf9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items): create_rfq_items(sq_doc, supplier, data) def create_rfq_items(sq_doc, supplier, data): - sq_doc.append('items', { - "item_code": data.item_code, - "item_name": data.item_name, - "description": data.description, - "qty": data.qty, - "rate": data.rate, - "conversion_factor": data.conversion_factor if data.conversion_factor else None, - "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), - "warehouse": data.warehouse or '', + args = {} + + for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor', + 'warehouse', 'material_request', 'material_request_item', 'stock_qty']: + args[field] = data.get(field) + + args.update({ "request_for_quotation_item": data.name, - "request_for_quotation": data.parent + "request_for_quotation": data.parent, + "supplier_part_no": frappe.db.get_value("Item Supplier", + {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no") }) + sq_doc.append('items', args) + @frappe.whitelist() def get_pdf(doctype, name, supplier): doc = get_rfq_doc(doctype, name, supplier) diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html index 383413103e1..3cfb8d8440c 100644 --- a/erpnext/templates/includes/transaction_row.html +++ b/erpnext/templates/includes/transaction_row.html @@ -13,9 +13,11 @@ {{ doc.items_preview }}
    -
    - {{ doc.get_formatted("grand_total") }} -
    + {% if doc.get('grand_total') %} +
    + {{ doc.get_formatted("grand_total") }} +
    + {% endif %}
    Link From a94b89727c550cc1eaea86379a2cb4d53297a8b8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 May 2021 20:11:15 +0530 Subject: [PATCH 130/376] feat: subcontract code refactor and enhancement --- .../purchase_invoice/test_purchase_invoice.py | 2 + .../doctype/purchase_order/purchase_order.js | 38 +- .../purchase_order/purchase_order.json | 3 +- .../doctype/purchase_order/purchase_order.py | 59 +- .../purchase_order/test_purchase_order.py | 100 +-- .../purchase_order_item_supplied.json | 45 +- .../purchase_receipt_item_supplied.json | 17 +- .../subcontract_order_summary/__init__.py | 0 .../subcontract_order_summary.js | 45 ++ .../subcontract_order_summary.json | 32 + .../subcontract_order_summary.py | 158 +++++ erpnext/controllers/buying_controller.py | 427 +------------ erpnext/controllers/subcontracting.py | 342 ++++++++++ erpnext/manufacturing/doctype/bom/test_bom.py | 2 + erpnext/stock/doctype/bin/bin.py | 4 +- .../item_alternative/test_item_alternative.py | 5 + .../purchase_receipt/purchase_receipt.js | 2 + .../purchase_receipt/purchase_receipt.json | 5 +- .../purchase_receipt/purchase_receipt.py | 2 + .../purchase_receipt/test_purchase_receipt.py | 4 + .../stock/doctype/stock_entry/stock_entry.js | 4 + .../doctype/stock_entry/stock_entry.json | 15 +- .../stock/doctype/stock_entry/stock_entry.py | 79 ++- erpnext/tests/test_subcontracting.py | 583 ++++++++++++++++++ 24 files changed, 1418 insertions(+), 555 deletions(-) create mode 100644 erpnext/buying/report/subcontract_order_summary/__init__.py create mode 100644 erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js create mode 100644 erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json create mode 100644 erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py create mode 100644 erpnext/controllers/subcontracting.py create mode 100644 erpnext/tests/test_subcontracting.py diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 503dda77282..ff433b962ff 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(actual_qty_0, get_qty_after_transaction()) def test_subcontracting_via_purchase_invoice(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + update_backflush_based_on('BOM') make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 0f6d927b362..440cde6d9e2 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -53,6 +53,38 @@ frappe.ui.form.on("Purchase Order", { } else { frm.set_value("tax_withholding_category", frm.supplier_tds); } + }, + + refresh: function(frm) { + frm.trigger('get_materials_from_supplier'); + }, + + get_materials_from_supplier: function(frm) { + let po_details = []; + + if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) { + frm.doc.supplied_items.forEach(d => { + if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + po_details.push(d.name) + } + }); + } + + if (po_details && po_details.length) { + frm.add_custom_button(__('Return of Components'), () => { + frm.call({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier', + freeze_message: __('Creating Stock Entry'), + args: { purchase_order: frm.doc.name, po_details: po_details }, + callback: function(r) { + if (r && r.message) { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); + }, __('Create')); + } } }); @@ -217,7 +249,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( }, has_unsupplied_items: function() { - return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty) + return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty) }, make_stock_entry: function() { @@ -513,12 +545,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( ], primary_action: function() { var data = d.get_values(); + var content_msg = 'Reason for hold: ' + data.reason_for_hold; + frappe.call({ method: "frappe.desk.form.utils.add_comment", args: { reference_doctype: me.frm.doctype, reference_name: me.frm.docname, - content: __('Reason for hold:') + " " +data.reason_for_hold, + content: __(content_msg), comment_email: frappe.session.user, comment_by: frappe.session.user_fullname }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 41668c62919..bb0ad60cabc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -609,6 +609,7 @@ "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", + "no_copy": 1, "oldfieldname": "po_raw_material_details", "oldfieldtype": "Table", "options": "Purchase Order Item Supplied", @@ -1377,7 +1378,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-04-19 00:55:30.781375", + "modified": "2021-05-30 15:17:53.663648", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2629ba7d61e..724f863e0f7 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -503,9 +503,10 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions @frappe.whitelist() def make_rm_stock_entry(purchase_order, rm_items): + rm_items_list = rm_items if isinstance(rm_items, string_types): rm_items_list = json.loads(rm_items) - else: + elif not rm_items: frappe.throw(_("No Items available for transfer")) if rm_items_list: @@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items): 'qty': rm_item_data["qty"], 'from_warehouse': rm_item_data["warehouse"], 'stock_uom': rm_item_data["stock_uom"], + 'serial_no': rm_item_data.get('serial_no'), + 'batch_no': rm_item_data.get('batch_no'), 'main_item_code': rm_item_data["item_code"], 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') } @@ -582,3 +585,57 @@ def update_status(status, name): def make_inter_company_sales_order(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction return make_inter_company_transaction("Purchase Order", source_name, target_doc) + +@frappe.whitelist() +def get_materials_from_supplier(purchase_order, po_details): + if isinstance(po_details, string_types): + po_details = json.loads(po_details) + + doc = frappe.get_cached_doc('Purchase Order', purchase_order) + doc.initialized_fields() + doc.purchase_orders = [doc.name] + doc.get_available_materials() + + if not doc.available_materials: + frappe.throw(_('Materials are already received against the purchase order {0}') + .format(purchase_order)) + + return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details) + +def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details): + ste_doc = frappe.new_doc('Stock Entry') + ste_doc.purpose = 'Material Transfer' + ste_doc.purchase_order = po_doc.name + ste_doc.company = po_doc.company + ste_doc.is_return = 1 + + for key, value in available_materials.items(): + if not value.qty: + continue + + if value.batch_no: + for batch_no, qty in value.batch_no.items(): + add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) + else: + add_items_in_ste(ste_doc, value, value.qty, po_details) + + ste_doc.set_stock_entry_type() + ste_doc.calculate_rate_and_amount() + + return ste_doc + +def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): + item = ste_doc.append('items', row.item_details) + + po_detail = list(set(row.po_details).intersection(po_details)) + item.update({ + 'qty': qty, + 'batch_no': batch_no, + 'basic_rate': row.item_details['rate'], + 'po_detail': po_detail[0] if po_detail else '', + 's_warehouse': row.item_details['t_warehouse'], + 't_warehouse': row.item_details['s_warehouse'], + 'item_code': row.item_details['rm_item_code'], + 'subcontracted_item': row.item_details['main_item_code'], + 'serial_no': '\n'.join(row.serial_no) if row.serial_no else '' + }) \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 3b9f8e9775a..33d19714510 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.batch.test_batch import make_new_batch -from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) def test_exploded_items_in_subcontracted(self): - item_code = "_Test Subcontracted FG Item 1" + item_code = "_Test Subcontracted FG Item 11" make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, @@ -853,76 +852,6 @@ class TestPurchaseOrder(unittest.TestCase): update_backflush_based_on("BOM") - def test_backflushed_based_on_for_multiple_batches(self): - item_code = "_Test Subcontracted FG Item 2" - make_item('Sub Contracted Raw Material 2', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1 - }) - - make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1, - raw_materials=["Sub Contracted Raw Material 2"]) - - update_backflush_based_on("Material Transferred for Subcontract") - - order_qty = 500 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") - - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100) - - rm_items = [ - {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item", - "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}] - - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.submit() - - for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]: - make_new_batch(batch_id=batch, item_code=item_code) - - pr = make_purchase_receipt(po.name) - - # partial receipt - pr.get('items')[0].qty = 30 - pr.get('items')[0].batch_no = "ABCD1" - - purchase_order = po.name - purchase_order_item = po.items[0].name - - for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items(): - pr.append("items", { - "item_code": pr.get('items')[0].item_code, - "item_name": pr.get('items')[0].item_name, - "uom": pr.get('items')[0].uom, - "stock_uom": pr.get('items')[0].stock_uom, - "warehouse": pr.get('items')[0].warehouse, - "conversion_factor": pr.get('items')[0].conversion_factor, - "cost_center": pr.get('items')[0].cost_center, - "rate": pr.get('items')[0].rate, - "qty": qty, - "batch_no": batch_no, - "purchase_order": purchase_order, - "purchase_order_item": purchase_order_item - }) - - pr.submit() - - pr1 = make_purchase_receipt(po.name) - pr1.get('items')[0].qty = 300 - pr1.get('items')[0].batch_no = "ABCD1" - pr1.save() - - pr_key = ("Sub Contracted Raw Material 2", po.name) - consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key) - - self.assertTrue(pr1.supplied_items[0].consumed_qty > 0) - self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty)) - - update_backflush_based_on("BOM") - def test_supplied_qty_against_subcontracted_po(self): item_code = "_Test Subcontracted FG Item 5" make_item('Sub Contracted Raw Material 4', { @@ -1117,22 +1046,29 @@ def create_purchase_order(**args): po.conversion_factor = args.conversion_factor or 1 po.supplier_warehouse = args.supplier_warehouse or None - po.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 10, - "rate": args.rate or 500, - "schedule_date": add_days(nowdate(), 1), - "include_exploded_items": args.get('include_exploded_items', 1), - "against_blanket_order": args.against_blanket_order - }) + if args.rm_items: + for row in args.rm_items: + po.append("items", row) + else: + po.append("items", { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 10, + "rate": args.rate or 500, + "schedule_date": add_days(nowdate(), 1), + "include_exploded_items": args.get('include_exploded_items', 1), + "against_blanket_order": args.against_blanket_order + }) + + po.set_missing_values() if not args.do_not_save: po.insert() if not args.do_not_submit: if po.is_subcontracted == "Yes": supp_items = po.get("supplied_items") for d in supp_items: - d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" + if not d.reserve_warehouse: + d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" po.submit() return po diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index d7ea9c1ccc8..505ecd84c51 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -6,21 +6,25 @@ "engine": "InnoDB", "field_order": [ "main_item_code", - "bom_detail_no", + "rm_item_code", + "column_break_3", "stock_uom", + "reserve_warehouse", "conversion_factor", "column_break_6", - "rm_item_code", + "bom_detail_no", "reference_name", - "reserve_warehouse", "section_break2", "rate", "col_break2", "amount", "section_break1", "required_qty", + "supplied_qty", "col_break1", - "supplied_qty" + "returned_qty", + "total_supplied_qty", + "consumed_qty" ], "fields": [ { @@ -125,6 +129,8 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Supplied Qty", + "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -142,13 +148,42 @@ { "fieldname": "col_break2", "fieldtype": "Column Break" + }, + { + "fieldname": "consumed_qty", + "fieldtype": "Float", + "label": "Consumed Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "total_supplied_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Total Supplied Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-06-01 00:41:54.123436", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index dc00bca5cc5..d8c37f58819 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -6,10 +6,11 @@ "engine": "InnoDB", "field_order": [ "main_item_code", - "description", + "rm_item_code", + "item_name", "bom_detail_no", "col_break1", - "rm_item_code", + "description", "stock_uom", "conversion_factor", "reference_name", @@ -52,7 +53,6 @@ "fieldname": "description", "fieldtype": "Text Editor", "in_global_search": 1, - "in_list_view": 1, "label": "Description", "oldfieldname": "description", "oldfieldtype": "Data", @@ -87,12 +87,13 @@ "read_only": 1 }, { + "columns": 2, "fieldname": "consumed_qty", "fieldtype": "Float", + "in_list_view": 1, "label": "Consumed Qty", "oldfieldname": "consumed_qty", "oldfieldtype": "Currency", - "read_only": 1, "reqd": 1 }, { @@ -183,12 +184,18 @@ { "fieldname": "col_break4", "fieldtype": "Column Break" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-05-29 17:22:14.977117", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", diff --git a/erpnext/buying/report/subcontract_order_summary/__init__.py b/erpnext/buying/report/subcontract_order_summary/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js new file mode 100644 index 00000000000..5ba52f1b21e --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -0,0 +1,45 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Subcontract Order Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, + { + label: __("Purchase Order"), + fieldname: "name", + fieldtype: "Link", + options: "Purchase Order", + get_query: function() { + return { + filters: { + docstatus: 1, + is_subcontracted: 'Yes', + company: frappe.query_report.get_filter_value('company') + } + } + } + } + ] +}; diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json new file mode 100644 index 00000000000..526a8d8ad01 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-05-31 14:43:32.417694", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-05-31 14:43:32.417694", + "modified_by": "Administrator", + "module": "Buying", + "name": "Subcontract Order Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Subcontract Order Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py new file mode 100644 index 00000000000..8b08d2a2840 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -0,0 +1,158 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_data(report_filters): + data = [] + orders = get_subcontracted_orders(report_filters) + + if orders: + supplied_items = get_supplied_items(orders, report_filters) + po_details = prepare_subcontracted_data(orders, supplied_items) + get_subcontracted_data(po_details, data) + + return data + +def get_subcontracted_orders(report_filters): + fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`', + '`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`', + '`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`'] + + filters = get_filters(report_filters) + + return frappe.get_all('Purchase Order', fields = fields, filters=filters) or [] + +def get_filters(report_filters): + filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'], + ['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]] + + for field in ['name', 'company']: + if report_filters.get(field): + filters.append(['Purchase Order', field, '=', report_filters.get(field)]) + + return filters + +def get_supplied_items(orders, report_filters): + if not orders: + return [] + + fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty', + 'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name'] + + filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1} + + supplied_items = {} + for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters): + new_key = (row.parent, row.reference_name, row.main_item_code) + + supplied_items.setdefault(new_key, []).append(row) + + return supplied_items + +def prepare_subcontracted_data(orders, supplied_items): + po_details = {} + for row in orders: + key = (row.po_id, row.name, row.item_code) + if key not in po_details: + po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []})) + + details = po_details[key] + + if supplied_items.get(key): + for supplied_item in supplied_items[key]: + details['supplied_items'].append(supplied_item) + + return po_details + +def get_subcontracted_data(po_details, data): + for key, details in po_details.items(): + res = details.po_item + for index, row in enumerate(details.supplied_items): + if index != 0: + res = {} + + res.update(row) + data.append(res) + +def get_columns(): + return [ + { + "label": _("Id"), + "fieldname": "po_id", + "fieldtype": "Link", + "options": "Purchase Order", + "width": 100 + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "width": 80 + }, + { + "label": _("Subcontracted Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140 + }, + { + "label": _("Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 70 + }, + { + "label": _("Received"), + "fieldname": "received_qty", + "fieldtype": "Float", + "width": 80 + }, + { + "label": _("Supplied Item"), + "fieldname": "rm_item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140 + }, + { + "label": _("Required Qty"), + "fieldname": "required_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Supplied Qty"), + "fieldname": "supplied_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Returned Qty"), + "fieldname": "returned_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Total Supplied"), + "fieldname": "total_supplied_qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", + "fieldtype": "Float", + "width": 110 + }, + ] \ No newline at end of file diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 20f54457259..1907885717b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details from erpnext.stock.get_item_details import get_conversion_factor from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.stock.stock_ledger import get_valuation_rate -from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos from frappe.contacts.doctype.address.address import get_address_display from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget -from erpnext.controllers.stock_controller import StockController from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.stock.utils import get_incoming_rate -class BuyingController(StockController): +from erpnext.controllers.stock_controller import StockController +from erpnext.controllers.subcontracting import Subcontracting + +class BuyingController(StockController, Subcontracting): def get_feed(self): if self.get("supplier_name"): @@ -256,7 +257,7 @@ class BuyingController(StockController): supplied_items_cost = 0.0 for d in self.get("supplied_items"): if d.reference_name == item_row_id: - if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'): + if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'): rate = get_incoming_rate({ "item_code": d.rm_item_code, "warehouse": self.supplier_warehouse, @@ -298,23 +299,7 @@ class BuyingController(StockController): def create_raw_materials_supplied(self, raw_material_table): if self.is_subcontracted=="Yes": - parent_items = [] - backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings", - "backflush_raw_materials_of_subcontract_based_on") - if (self.doctype == 'Purchase Receipt' and - backflush_raw_materials_based_on != 'BOM'): - self.update_raw_materials_supplied_based_on_stock_entries() - else: - for item in self.get("items"): - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - item.rm_supp_cost = 0.0 - if item.bom and item.item_code in self.sub_contracted_items: - self.update_raw_materials_supplied_based_on_bom(item, raw_material_table) - - if [item.item_code, item.name] not in parent_items: - parent_items.append([item.item_code, item.name]) - - self.cleanup_raw_materials_supplied(parent_items, raw_material_table) + self.set_materials_for_subcontracted_items(raw_material_table) elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: for item in self.get("items"): @@ -323,176 +308,6 @@ class BuyingController(StockController): if self.is_subcontracted == "No" and self.get("supplied_items"): self.set('supplied_items', []) - def update_raw_materials_supplied_based_on_stock_entries(self): - self.set('supplied_items', []) - - purchase_orders = set(d.purchase_order for d in self.items) - - # qty of raw materials backflushed (for each item per purchase order) - backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders) - - # qty of "finished good" item yet to be received - qty_to_be_received_map = get_qty_to_be_received(purchase_orders) - - for item in self.get('items'): - if not item.purchase_order: - continue - - # reset raw_material cost - item.rm_supp_cost = 0 - - # qty of raw materials transferred to the supplier - transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code) - - non_stock_items = get_non_stock_items(item.purchase_order, item.item_code) - - item_key = '{}{}'.format(item.item_code, item.purchase_order) - - fg_yet_to_be_received = qty_to_be_received_map.get(item_key) - - if not fg_yet_to_be_received: - frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}") - .format(item.idx, frappe.bold(item.item_code), - frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)), - title=_("Limit Crossed")) - - transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) - # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) - - for raw_material in transferred_raw_materials + non_stock_items: - rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order) - raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) - - consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_no', '') - consumed_batch_nos = raw_material_data.get('batch_nos', '') - - transferred_qty = raw_material.qty - - rm_qty_to_be_consumed = transferred_qty - consumed_qty - - # backflush all remaining transferred qty in the last Purchase Receipt - if fg_yet_to_be_received == item.qty: - qty = rm_qty_to_be_consumed - else: - qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty - - if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'): - qty = frappe.utils.ceil(qty) - - if qty > rm_qty_to_be_consumed: - qty = rm_qty_to_be_consumed - - if not qty: continue - - if raw_material.serial_nos: - set_serial_nos(raw_material, consumed_serial_nos, qty) - - if raw_material.batch_nos: - backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {}) - - batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, - qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) - - for batch_data in batches_qty: - qty = batch_data['qty'] - raw_material.batch_no = batch_data['batch'] - if qty > 0: - self.append_raw_material_to_be_backflushed(item, raw_material, qty) - else: - self.append_raw_material_to_be_backflushed(item, raw_material, qty) - - def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty): - rm = self.append('supplied_items', {}) - rm.update(raw_material_data) - - if not rm.main_item_code: - rm.main_item_code = fg_item_row.item_code - - rm.reference_name = fg_item_row.name - rm.required_qty = qty - rm.consumed_qty = qty - - def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): - exploded_item = 1 - if hasattr(item, 'include_exploded_items'): - exploded_item = item.get('include_exploded_items') - - bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item) - - used_alternative_items = [] - if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order: - used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order) - - raw_materials_cost = 0 - items = list(set([d.item_code for d in bom_items])) - item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse - from `tabItem` i, `tabItem Default` id - where id.parent=i.name and id.company=%s and i.name in ({0})""" - .format(", ".join(["%s"] * len(items))), [self.company] + items)) - - for bom_item in bom_items: - if self.doctype == "Purchase Order": - reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code) - if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company: - reserve_warehouse = None - - conversion_factor = item.conversion_factor - if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and - bom_item.item_code in used_alternative_items): - alternative_item_data = used_alternative_items.get(bom_item.item_code) - bom_item.item_code = alternative_item_data.item_code - bom_item.item_name = alternative_item_data.item_name - bom_item.stock_uom = alternative_item_data.stock_uom - conversion_factor = alternative_item_data.conversion_factor - bom_item.description = alternative_item_data.description - - # check if exists - exists = 0 - for d in self.get(raw_material_table): - if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \ - and d.reference_name == item.name: - rm, exists = d, 1 - break - - if not exists: - rm = self.append(raw_material_table, {}) - - required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) * - flt(conversion_factor), rm.precision("required_qty")) - rm.reference_name = item.name - rm.bom_detail_no = bom_item.name - rm.main_item_code = item.item_code - rm.rm_item_code = bom_item.item_code - rm.stock_uom = bom_item.stock_uom - rm.required_qty = required_qty - rm.rate = bom_item.rate - rm.conversion_factor = conversion_factor - - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - rm.consumed_qty = required_qty - rm.description = bom_item.description - if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no: - rm.batch_no = item.batch_no - elif not rm.reserve_warehouse: - rm.reserve_warehouse = reserve_warehouse - - def cleanup_raw_materials_supplied(self, parent_items, raw_material_table): - """Remove all those child items which are no longer present in main item table""" - delete_list = [] - for d in self.get(raw_material_table): - if [d.main_item_code, d.reference_name] not in parent_items: - # mark for deletion from doclist - delete_list.append(d) - - # delete from doclist - if delete_list: - rm_supplied_details = self.get(raw_material_table) - self.set(raw_material_table, []) - for d in rm_supplied_details: - if d not in delete_list: - self.append(raw_material_table, d) - @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): @@ -867,104 +682,6 @@ class BuyingController(StockController): else: validate_item_type(self, "is_purchase_item", "purchase") - -def get_items_from_bom(item_code, bom, exploded_item=1): - doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" - - bom_items = frappe.db.sql("""select t2.item_code, t2.name, - t2.rate, t2.stock_uom, t2.source_warehouse, t2.description, - t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit - from - `tabBOM` t1, `tab{0}` t2, tabItem t3 - where - t2.parent = t1.name and t1.item = %s - and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s - and t2.sourced_by_supplier = 0 - and t2.item_code = t3.name""".format(doctype), - (item_code, bom), as_dict=1) - - if not bom_items: - msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1) - - return bom_items - -def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): - common_query = """ - SELECT - sed.item_code AS rm_item_code, - SUM(sed.qty) AS qty, - sed.description, - sed.stock_uom, - sed.subcontracted_item AS main_item_code, - {serial_no_concat_syntax} AS serial_nos, - {batch_no_concat_syntax} AS batch_nos - FROM `tabStock Entry` se,`tabStock Entry Detail` sed - WHERE - se.name = sed.parent - AND se.docstatus=1 - AND se.purpose='Send to Subcontractor' - AND se.purchase_order = %s - AND IFNULL(sed.t_warehouse, '') != '' - AND IFNULL(sed.subcontracted_item, '') in ('', %s) - GROUP BY sed.item_code, sed.subcontracted_item - """ - raw_materials = frappe.db.multisql({ - 'mariadb': common_query.format( - serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)", - batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)" - ), - 'postgres': common_query.format( - serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')", - batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')" - ) - }, (purchase_order, fg_item), as_dict=1) - - return raw_materials - -def get_backflushed_subcontracted_raw_materials(purchase_orders): - purchase_receipts = frappe.get_all("Purchase Receipt Item", - fields = ["purchase_order", "item_code", "name", "parent"], - filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))}) - - distinct_purchase_receipts = {} - for pr in purchase_receipts: - key = (pr.purchase_order, pr.item_code, pr.parent) - distinct_purchase_receipts.setdefault(key, []).append(pr.name) - - backflushed_raw_materials_map = frappe._dict() - for args, references in iteritems(distinct_purchase_receipts): - purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references) - - for data in purchase_receipt_supplied_items: - pr_key = (data.rm_item_code, data.main_item_code, args[0]) - if pr_key not in backflushed_raw_materials_map: - backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ - "qty": 0.0, - "serial_no": [], - "batch_no": [], - "consumed_batch": {} - })) - - row = backflushed_raw_materials_map.get(pr_key) - row.qty += data.consumed_qty - - for field in ["serial_no", "batch_no"]: - if data.get(field): - row[field].append(data.get(field)) - - if data.get("batch_no"): - if data.get("batch_no") in row.consumed_batch: - row.consumed_batch[data.get("batch_no")] += data.consumed_qty - else: - row.consumed_batch[data.get("batch_no")] = data.consumed_qty - - return backflushed_raw_materials_map - -def get_supplied_items(item_code, purchase_receipt, references): - return frappe.get_all("Purchase Receipt Item Supplied", - fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"], - filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)}) - def get_asset_item_details(asset_items): asset_items_data = {} for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], @@ -996,135 +713,3 @@ def validate_item_type(doc, fieldname, message): error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) frappe.throw(error_message) - -def get_qty_to_be_received(purchase_orders): - return frappe._dict(frappe.db.sql(""" - SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key, - SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received - FROM `tabPurchase Order Item` poi - WHERE - poi.`parent` in %s - GROUP BY poi.`item_code`, poi.`parent` - HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`) - """, (purchase_orders))) - -def get_non_stock_items(purchase_order, fg_item_code): - return frappe.db.sql(""" - SELECT - pois.main_item_code, - pois.rm_item_code, - item.description, - pois.required_qty AS qty, - pois.rate, - 1 as non_stock_item, - pois.stock_uom - FROM `tabPurchase Order Item Supplied` pois, `tabItem` item - WHERE - pois.`rm_item_code` = item.`name` - AND item.is_stock_item = 0 - AND pois.`parent` = %s - AND pois.`main_item_code` = %s - """, (purchase_order, fg_item_code), as_dict=1) - - -def set_serial_nos(raw_material, consumed_serial_nos, qty): - serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \ - set(get_serial_nos(consumed_serial_nos)) - if serial_nos and qty <= len(serial_nos): - raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)]) - -def get_transferred_batch_qty_map(purchase_order, fg_item): - # returns - # { - # (item_code, fg_code): { - # batch1: 10, # qty - # batch2: 16 - # }, - # } - transferred_batch_qty_map = {} - transferred_batches = frappe.db.sql(""" - SELECT - sed.batch_no, - SUM(sed.qty) AS qty, - sed.item_code, - sed.subcontracted_item - FROM `tabStock Entry` se,`tabStock Entry Detail` sed - WHERE - se.name = sed.parent - AND se.docstatus=1 - AND se.purpose='Send to Subcontractor' - AND se.purchase_order = %s - AND ifnull(sed.subcontracted_item, '') in ('', %s) - AND sed.batch_no IS NOT NULL - GROUP BY - sed.batch_no, - sed.item_code - """, (purchase_order, fg_item), as_dict=1) - - for batch_data in transferred_batches: - key = ((batch_data.item_code, fg_item) - if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) - transferred_batch_qty_map.setdefault(key, OrderedDict()) - transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty - - return transferred_batch_qty_map - -def get_backflushed_batch_qty_map(purchase_order, fg_item): - # returns - # { - # (item_code, fg_code): { - # batch1: 10, # qty - # batch2: 16 - # }, - # } - backflushed_batch_qty_map = {} - backflushed_batches = frappe.db.sql(""" - SELECT - pris.batch_no, - SUM(pris.consumed_qty) AS qty, - pris.rm_item_code AS item_code - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris - WHERE - pr.name = pri.parent - AND pri.parent = pris.parent - AND pri.purchase_order = %s - AND pri.item_code = pris.main_item_code - AND pr.docstatus = 1 - AND pris.main_item_code = %s - AND pris.batch_no IS NOT NULL - GROUP BY - pris.rm_item_code, pris.batch_no - """, (purchase_order, fg_item), as_dict=1) - - for batch_data in backflushed_batches: - backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) - backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty - - return backflushed_batch_qty_map - -def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po): - # Returns available batches to be backflushed based on requirements - transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) - if not transferred_batches: - transferred_batches = transferred_batch_qty_map.get((item_code, po), {}) - - available_batches = [] - - for (batch, transferred_qty) in transferred_batches.items(): - backflushed_qty = backflushed_batches.get(batch, 0) - available_qty = transferred_qty - backflushed_qty - - if available_qty >= required_qty: - available_batches.append({'batch': batch, 'qty': required_qty}) - break - elif available_qty != 0: - available_batches.append({'batch': batch, 'qty': available_qty}) - required_qty -= available_qty - - for row in available_batches: - if backflushed_batches.get(row.get('batch'), 0) > 0: - backflushed_batches[row.get('batch')] += row.get('qty') - else: - backflushed_batches[row.get('batch')] = row.get('qty') - - return available_batches diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py new file mode 100644 index 00000000000..fe775766da5 --- /dev/null +++ b/erpnext/controllers/subcontracting.py @@ -0,0 +1,342 @@ +from __future__ import unicode_literals + +import frappe +from frappe import _ +from frappe.utils import flt, cint +from collections import defaultdict +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + +class Subcontracting(object): + def set_materials_for_subcontracted_items(self, raw_material_table): + if self.doctype == 'Purchase Invoice' and not self.update_stock: + return + + self.raw_material_table = raw_material_table + self.identify_change_in_item_table() + self.prepare_supplied_items() + self.validate_consumed_qty() + + def prepare_supplied_items(self): + self.initialized_fields() + self.get_purchase_orders() + self.get_pending_qty_to_receive() + self.get_available_materials() + self.remove_changed_rows() + self.set_supplied_items() + + def initialized_fields(self): + self.available_materials = frappe._dict() + self.alternative_item_details = frappe._dict() + self.get_backflush_based_on() + + def get_backflush_based_on(self): + self.backflush_based_on = frappe.db.get_single_value("Buying Settings", + "backflush_raw_materials_of_subcontract_based_on") + + def get_purchase_orders(self): + self.purchase_orders = [] + + if self.doctype == 'Purchase Order': + return + + self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] + + def identify_change_in_item_table(self): + self.changed_name = [] + + if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table): + self.set(self.raw_material_table, []) + return + + item_dict = self.get_data_before_save() + if not item_dict: + return True + + for n_row in self.items: + if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]: + self.changed_name.append(n_row.name) + + if item_dict.get(n_row.name): + del item_dict[n_row.name] + + self.changed_name.extend(item_dict.keys()) + + def get_data_before_save(self): + item_dict = {} + if self.doctype == 'Purchase Receipt' and self._doc_before_save: + for row in self._doc_before_save.get('items'): + item_dict[row.name] = (row.item_code, row.qty) + + return item_dict + + def get_available_materials(self): + ''' Get the available raw materials which has been transferred to the supplier. + available_materials = { + (item_code, subcontracted_item, purchase_order): { + 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details + } + } + ''' + if not self.purchase_orders: + return + + for row in self.get_transferred_items(): + key = (row.rm_item_code, row.main_item_code, row.purchase_order) + + if key not in self.available_materials: + self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [], + 'batch_no': defaultdict(float), 'item_details': row, 'po_details': []}) + ) + + details = self.available_materials[key] + details.qty += row.qty + details.po_details.append(row.po_detail) + + if row.serial_no: + details.serial_no.extend(get_serial_nos(row.serial_no)) + + if row.batch_no: + details.batch_no[row.batch_no] += row.qty + + self.set_alternative_item_details(row) + + for doctype in ['Purchase Receipt', 'Purchase Invoice']: + self.remove_consumed_materials(doctype) + + def remove_consumed_materials(self, doctype, return_consumed_items=False): + '''Deduct the consumed materials from the available materials.''' + + pr_items = self.get_received_items(doctype) + if not pr_items: + return ([], {}) if return_consumed_items else None + + pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items} + consumed_materials = self.get_consumed_items(doctype, pr_items.keys()) + + if return_consumed_items: + return (consumed_materials, pr_items) + + for row in consumed_materials: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + if not self.available_materials.get(key): + continue + + self.available_materials[key]['qty'] -= row.consumed_qty + if row.serial_no: + self.available_materials[key]['serial_no'] = list( + set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no)) + ) + + if row.batch_no: + self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty + + def get_transferred_items(self): + fields = ['`tabStock Entry`.`purchase_order`'] + alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'} + + child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount', + 'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor', + 's_warehouse', 't_warehouse', 'item_group', 'po_detail'] + + if self.backflush_based_on == 'BOM': + child_table_fields.append('original_item') + + for field in child_table_fields: + fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}') + + filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'], + ['Stock Entry', 'purchase_order', 'in', self.purchase_orders]] + + return frappe.get_all('Stock Entry', fields = fields, filters=filters) + + def get_received_items(self, doctype): + fields = [] + self.po_field = 'purchase_order' if doctype == 'Purchase Receipt' else 'po_detail' + + for field in ['name', self.po_field, 'parent']: + fields.append(f'`tab{doctype} Item`.`{field}`') + + filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]] + if doctype == 'Purchase Invoice': + filters.append(['Purchase Invoice', 'update_stock', "=", 1]) + + return frappe.get_all(f'{doctype}', fields = fields, filters = filters) + + def get_consumed_items(self, doctype, pr_items): + return frappe.get_all(f'{doctype} Item Supplied', + fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], + filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items))}) + + def set_alternative_item_details(self, row): + if row.get('original_item'): + self.alternative_item_details[row.get('original_item')] = row + + def get_pending_qty_to_receive(self): + '''Get qty to be received against the purchase order.''' + + self.qty_to_be_received = defaultdict(float) + + if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders: + for row in frappe.get_all('Purchase Order Item', + fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'], + filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}): + + self.qty_to_be_received[(row.item_code, row.parent)] += row.qty + + def get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item' + fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit'] + + alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'} + for field in ['item_code', 'name', 'rate', 'stock_uom', + 'source_warehouse', 'description', 'item_name', 'stock_uom']: + fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}') + + filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1], + ['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]] + + return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] + + def remove_changed_rows(self): + if not self.changed_name: + return + + i=1 + self.set(self.raw_material_table, []) + for d in self._doc_before_save.supplied_items: + if d.reference_name in self.changed_name: + continue + + d.idx = i + self.append('supplied_items', d) + + i += 1 + + def set_supplied_items(self): + self.bom_items = {} + + has_supplied_items = True if self.get(self.raw_material_table) else False + for row in self.items: + if (self.doctype != 'Purchase Order' and ((self.changed_name and row.name not in self.changed_name) + or (has_supplied_items and not self.changed_name))): + continue + + if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': + for bom_item in self.get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): + qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor) + bom_item.main_item_code = row.item_code + self.update_reserve_warehouse(bom_item, row) + self.set_alternative_item(bom_item) + self.add_supplied_item(row, bom_item, qty) + + elif self.backflush_based_on != 'BOM': + for key, transfer_item in self.available_materials.items(): + if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0: + qty = self.get_qty_based_on_material_transfer(row, transfer_item) or 0 + transfer_item.qty -= qty + self.add_supplied_item(row, transfer_item.get('item_details'), qty) + + if self.qty_to_be_received: + self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty + + def update_reserve_warehouse(self, row, item): + if self.doctype == 'Purchase Order': + row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse) + + def get_qty_based_on_material_transfer(self, item_row, transfer_item): + key = (item_row.item_code, item_row.purchase_order) + + if self.qty_to_be_received == item_row.qty: + return transfer_item.qty + + if self.qty_to_be_received: + qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + if (transfer_item.serial_no or frappe.get_cached_value('UOM', + transfer_item.item_details.stock_uom, 'must_be_whole_number')): + return frappe.utils.ceil(qty) + + return qty + + def set_alternative_item(self, bom_item): + if self.alternative_item_details.get(bom_item.rm_item_code): + bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) + + def add_supplied_item(self, item_row, bom_item, qty): + bom_item.conversion_factor = item_row.conversion_factor + rm_obj = self.append(self.raw_material_table, bom_item) + rm_obj.reference_name = item_row.name + + if self.doctype == 'Purchase Order': + rm_obj.required_qty = qty + else: + self.set_batch_nos(bom_item, item_row, rm_obj, qty) + + def set_batch_nos(self, bom_item, item_row, rm_obj, qty): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) + + if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): + for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): + if batch_qty >= qty: + self.set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) + self.available_materials[key]['batch_no'][batch_no] -= qty + return + + elif qty > 0 and batch_qty > 0: + qty -= batch_qty + new_rm_obj = self.append(self.raw_material_table, bom_item) + new_rm_obj.reference_name = item_row.name + self.set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) + self.available_materials[key]['batch_no'][batch_no] = 0 + else: + rm_obj.required_qty = qty + rm_obj.consumed_qty = qty + self.set_serial_nos(item_row, rm_obj) + + def set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): + rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, 'required_qty': qty}) + self.set_serial_nos(item_row, rm_obj) + + def set_serial_nos(self, item_row, rm_obj): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) + if (self.available_materials.get(key) and self.available_materials[key]['serial_no']): + used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)] + rm_obj.serial_no = '\n'.join(used_serial_nos) + + # Removed the used serial nos from the list + for sn in used_serial_nos: + self.available_materials[key]['serial_no'].remove(sn) + + def set_consumed_qty_in_po(self): + if self.is_subcontracted != 'Yes': + return + + self.get_purchase_orders() + consumed_items, pr_items = self.remove_consumed_materials(self.doctype, return_consumed_items=True) + + itemwise_consumed_qty = defaultdict(float) + for row in consumed_items: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + itemwise_consumed_qty[key] += row.consumed_qty + + self.update_consumed_qty_in_po(itemwise_consumed_qty) + + def update_consumed_qty_in_po(self, itemwise_consumed_qty): + fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name'] + filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)} + + for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'): + key = (row.rm_item_code, row.main_item_code, row.parent) + consumed_qty = itemwise_consumed_qty.get(key, 0) + + if row.supplied_qty < consumed_qty: + consumed_qty = row.supplied_qty + + itemwise_consumed_qty[key] -= consumed_qty + frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) + + def validate_consumed_qty(self): + for row in self.get(self.raw_material_table): + if flt(row.consumed_qty) == 0.0 and row.get('serial_no'): + msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + + frappe.throw(_(msg),title=_('Consumed Items Qty Check')) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index e1cca9e3ef4..42b23f223d2 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update from six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.tests.test_subcontracting import set_backflush_based_on test_records = frappe.get_test_records('BOM') @@ -160,6 +161,7 @@ class TestBOM(unittest.TestCase): def test_subcontractor_sourced_item(self): item_code = "_Test Subcontracted FG Item 1" + set_backflush_based_on('Material Transferred for Subcontract') if not frappe.db.exists('Item', item_code): make_item(item_code, { diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 0514bd23942..43642013ce6 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -54,7 +54,7 @@ class Bin(Document): self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty")) self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty")) self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty")) - + self.set_projected_qty() self.db_update() @@ -115,7 +115,7 @@ class Bin(Document): #Get Transferred Entries materials_transferred = frappe.db.sql(""" select - ifnull(sum(transfer_qty),0) + ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0) from `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po where diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index d5700fe5147..8f76844bde0 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -18,6 +18,9 @@ class TestItemAlternative(unittest.TestCase): make_items() def test_alternative_item_for_subcontract_rm(self): + frappe.db.set_value('Buying Settings', None, + 'backflush_raw_materials_of_subcontract_based_on', 'BOM') + create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC', qty=5, rate=2000) create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', @@ -65,6 +68,8 @@ class TestItemAlternative(unittest.TestCase): status = True self.assertEqual(status, True) + frappe.db.set_value('Buying Settings', None, + 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract') def test_alternative_item_for_production_rm(self): create_stock_reconciliation(item_code='Alternate Item For A RW 1', diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index befdad96924..887b15a2115 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -41,6 +41,8 @@ frappe.ui.form.on("Purchase Receipt", { } }); + frm.set_df_property('supplied_items', 'cannot_add_rows', 1); + }, onload: function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index ad350d344f8..44fb736304f 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -514,8 +514,7 @@ "oldfieldname": "pr_raw_material_details", "oldfieldtype": "Table", "options": "Purchase Receipt Item Supplied", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "section_break0", @@ -1149,7 +1148,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2021-04-19 01:01:00.754119", + "modified": "2021-05-25 00:15:12.239017", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 83ba3244952..b8580f95a3a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -202,6 +202,7 @@ class PurchaseReceipt(BuyingController): self.make_gl_entries() self.repost_future_sle_and_gle() + self.set_consumed_qty_in_po() def check_next_docstatus(self): submit_rv = frappe.db.sql("""select t1.name @@ -233,6 +234,7 @@ class PurchaseReceipt(BuyingController): self.repost_future_sle_and_gle() self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.delete_auto_created_batches() + self.set_consumed_qty_in_po() @frappe.whitelist() def get_current_stock(self): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8d9b675bed3..95096d77d77 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -335,6 +335,10 @@ class TestPurchaseReceipt(unittest.TestCase): se2.cancel() se3.cancel() po.reload() + pr2.load_from_db() + pr2.cancel() + + po.load_from_db() po.cancel() def test_serial_no_supplier(self): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 1a25994b243..67083930272 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1079,6 +1079,10 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { } function attach_bom_items(bom_no) { + if (!bom_no) { + return + } + if (check_should_not_attach_bom_items(bom_no)) return frappe.db.get_doc("BOM",bom_no).then(bom => { const {name, items} = bom diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index a0b5457dd7d..523d332b8f4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -74,7 +74,8 @@ "total_amount", "job_card", "amended_from", - "credit_note" + "credit_note", + "is_return" ], "fields": [ { @@ -611,6 +612,16 @@ "fieldname": "apply_putaway_rule", "fieldtype": "Check", "label": "Apply Putaway Rule" + }, + { + "default": "0", + "fieldname": "is_return", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Return", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -618,7 +629,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-24 11:32:23.904307", + "modified": "2021-05-26 17:07:58.015737", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 560ceaa917b..213280870af 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -97,8 +97,7 @@ class StockEntry(StockController): update_serial_nos_after_submit(self, "items") self.update_work_order() self.validate_purchase_order() - if self.purchase_order and self.purpose == "Send to Subcontractor": - self.update_purchase_order_supplied_items() + self.update_purchase_order_supplied_items() self.make_gl_entries() @@ -117,9 +116,7 @@ class StockEntry(StockController): self.set_material_request_transfer_status('Completed') def on_cancel(self): - - if self.purchase_order and self.purpose == "Send to Subcontractor": - self.update_purchase_order_supplied_items() + self.update_purchase_order_supplied_items() if self.work_order and self.purpose == "Material Consumption for Manufacture": self.validate_work_order_status() @@ -1347,7 +1344,7 @@ class StockEntry(StockController): se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) for field in ["idx", "po_detail", "original_item", - "expense_account", "description", "item_name"]: + "expense_account", "description", "item_name", "serial_no", "batch_no"]: if item_dict[d].get(field): se_child.set(field, item_dict[d].get(field)) @@ -1400,33 +1397,26 @@ class StockEntry(StockController): .format(item.batch_no, item.item_code)) def update_purchase_order_supplied_items(self): - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" - select rm_item_code, reserve_warehouse - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup - where po.name = poitemsup.parent - and po.name = %s""", self.purchase_order)) + if (self.purchase_order and + (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)): - #Update Supplied Qty in PO Supplied Items + #Get PO Supplied Items Details + item_wh = frappe._dict(frappe.db.sql(""" + select rm_item_code, reserve_warehouse + from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup + where po.name = poitemsup.parent + and po.name = %s""", self.purchase_order)) - frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos - SET - pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0) - FROM - `tabStock Entry Detail` sed, `tabStock Entry` se - WHERE - pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code - AND pos.parent = se.purchase_order AND sed.docstatus = 1 - AND se.name = sed.parent and se.purchase_order = %(po)s - ), 0) - WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order}) + supplied_items = get_supplied_items(self.purchase_order) + for name, item in supplied_items.items(): + frappe.db.set_value('Purchase Order Item Supplied', name, item) - #Update reserved sub contracted quantity in bin based on Supplied Item Details and - for d in self.get("items"): - item_code = d.get('original_item') or d.get('item_code') - reserve_warehouse = item_wh.get(item_code) - stock_bin = get_bin(item_code, reserve_warehouse) - stock_bin.update_reserved_qty_for_sub_contracting() + #Update reserved sub contracted quantity in bin based on Supplied Item Details and + for d in self.get("items"): + item_code = d.get('original_item') or d.get('item_code') + reserve_warehouse = item_wh.get(item_code) + stock_bin = get_bin(item_code, reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting() def update_so_in_serial_number(self): so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"]) @@ -1480,7 +1470,7 @@ class StockEntry(StockController): cond += """ WHEN (parent = %s and name = %s) THEN %s """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) - if cond and stock_entries_child_list: + if stock_entries_child_list: frappe.db.sql(""" UPDATE `tabStock Entry Detail` SET transferred_qty = CASE {cond} END @@ -1751,3 +1741,30 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None): format(max_retain_qty, batch_no, item_code), alert=True) sample_quantity = qty_diff return sample_quantity + +def get_supplied_items(purchase_order): + fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`', + '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`'] + + filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]] + + supplied_item_details = {} + for row in frappe.get_all('Stock Entry', fields = fields, filters = filters): + if not row.po_detail: + continue + + key = row.po_detail + if key not in supplied_item_details: + supplied_item_details.setdefault(key, + frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0})) + + supplied_item = supplied_item_details[key] + + if row.is_return: + supplied_item.returned_qty += row.transfer_qty + else: + supplied_item.supplied_qty += row.transfer_qty + + supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty) + + return supplied_item_details \ No newline at end of file diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py new file mode 100644 index 00000000000..c1a458a6ddb --- /dev/null +++ b/erpnext/tests/test_subcontracting.py @@ -0,0 +1,583 @@ +from __future__ import unicode_literals +import frappe +import unittest +import copy +from frappe.utils import cint +from collections import defaultdict +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.buying.doctype.purchase_order.purchase_order import (make_rm_stock_entry, + make_purchase_receipt, get_materials_from_supplier) + +class TestSubcontracting(unittest.TestCase): + def setUp(self): + make_subcontract_items() + make_raw_materials() + make_bom_for_subcontracted_items() + + def test_po_with_bom(self): + ''' + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Create purchase receipt against the PO and check serial nos and batch no. + ''' + + set_backflush_based_on('BOM') + item_code = 'Subcontracted Item SA1' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 5, 'rate': 100}, + {'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 6, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 5}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 5}, + {'item_code': 'Subcontracted SRM Item 1', 'qty': 6}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 6}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 6} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + + for field in ['qty', 'serial_no', 'batch_no']: + if value.get(field): + transfer, consumed = (transferred_detais.get(field), value.get(field)) + if field == 'serial_no': + transfer, consumed = (sorted(transfer), sorted(consumed)) + + self.assertEqual(transfer, consumed) + + def test_po_with_material_transfer(self): + ''' + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. + - Create partial purchase receipt against the PO and check serial nos and batch no. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA1', 'qty': 5, 'rate': 100}, + {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA5', 'qty': 6, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, + {'item_code': 'Subcontracted SRM Item 5', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'}, + {'item_code': 'Subcontracted SRM Item 4', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + + make_stock_transfer_entry(po_no = po.name, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.remove(pr1.items[1]) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + + for field in ['qty', 'serial_no', 'batch_no']: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + pr2 = make_purchase_receipt(po.name) + pr2.submit() + + for key, value in get_supplied_items(pr2).items(): + transferred_detais = itemwise_details.get(key) + + for field in ['qty', 'serial_no', 'batch_no']: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + def test_subcontract_with_same_components_different_fg(self): + ''' + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of components for the item Subcontracted Item SA2. + - Create partial purchase receipt against the PO and check serial nos and batch no. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}, + {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA3', 'qty': 6, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA3'} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + + make_stock_transfer_entry(po_no = po.name, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 3 + pr1.remove(pr1.items[1]) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + self.assertEqual(value.qty, 4) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:4])) + + pr2 = make_purchase_receipt(po.name) + pr2.items[0].qty = 2 + pr2.remove(pr2.items[1]) + pr2.submit() + + for key, value in get_supplied_items(pr2).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 2) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[4:6])) + + pr3 = make_purchase_receipt(po.name) + pr3.submit() + for key, value in get_supplied_items(pr3).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 6) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[6:12])) + + def test_return_non_consumed_materials(self): + ''' + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. + - Create purchase receipt for full qty against the PO and change the qty of raw material. + - After that return the non consumed material back to the store from supplier's warehouse. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}] + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.save() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].serial_no = '\n'.join(sorted( + itemwise_details.get('Subcontracted SRM Item 2').get('serial_no')[0:5] + )) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + self.assertEqual(value.qty, 5) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:5])) + + po.load_from_db() + self.assertEqual(po.supplied_items[0].consumed_qty, 5) + doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items]) + self.assertEqual(doc.items[0].qty, 1) + self.assertEqual(doc.items[0].s_warehouse, '_Test Warehouse 1 - _TC') + self.assertEqual(doc.items[0].t_warehouse, '_Test Warehouse - _TC') + self.assertEqual(get_serial_nos(doc.items[0].serial_no), + itemwise_details.get(doc.items[0].item_code)['serial_no'][5:6]) + + def test_item_with_batch_based_on_bom(self): + ''' + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + ''' + + set_backflush_based_on('BOM') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + + def test_item_with_batch_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + self.assertEqual(value.qty, qty) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + + def test_partial_transfer_serial_no_components_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA2' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 5 + pr1.save() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3])) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + def test_partial_transfer_batch_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA6' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 5 + pr1.save() + + transferred_batch_no = '' + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + transferred_batch_no = details.batch_no + self.assertEqual(value.batch_no, details.batch_no) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0] + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + +def add_second_row_in_pr(pr): + item_dict = {} + for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom', + 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate']: + item_dict[column] = pr.items[0].get(column) + + pr.append('items', item_dict) + pr.set_missing_values() + +def get_supplied_items(pr_doc): + supplied_items = {} + for row in pr_doc.get('supplied_items'): + if row.rm_item_code not in supplied_items: + supplied_items.setdefault(row.rm_item_code, + frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + + details = supplied_items[row.rm_item_code] + update_item_details(row, details) + + return supplied_items + +def make_stock_in_entry(**args): + args = frappe._dict(args) + + items = {} + for row in args.rm_items: + row = frappe._dict(row) + + doc = make_stock_entry(target=row.warehouse or '_Test Warehouse - _TC', + item_code=row.item_code, qty=row.qty or 1, basic_rate=row.rate or 100) + + if row.item_code not in items: + items.setdefault(row.item_code, frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + + child_row = doc.items[0] + details = items[child_row.item_code] + update_item_details(child_row, details) + + return items + +def update_item_details(child_row, details): + details.qty += (child_row.get('qty') if child_row.doctype == 'Stock Entry Detail' + else child_row.get('consumed_qty')) + + if child_row.serial_no: + details.serial_no.extend(get_serial_nos(child_row.serial_no)) + + if child_row.batch_no: + details.batch_no[child_row.batch_no] += (child_row.get('qty') or child_row.get('consumed_qty')) + +def make_stock_transfer_entry(**args): + args = frappe._dict(args) + + items = [] + for row in args.rm_items: + row = frappe._dict(row) + + item = {'item_code': row.main_item_code or args.main_item_code, 'rm_item_code': row.item_code, + 'qty': row.qty or 1, 'item_name': row.item_code, 'rate': row.rate or 100, + 'stock_uom': row.stock_uom or 'Nos', 'warehouse': row.warehuose or '_Test Warehouse - _TC'} + + item_details = args.itemwise_details.get(row.item_code) + + if item_details and item_details.serial_no: + serial_nos = item_details.serial_no[0:cint(row.qty)] + item['serial_no'] = '\n'.join(serial_nos) + item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos)) + + if item_details and item_details.batch_no: + for batch_no, batch_qty in item_details.batch_no.items(): + if batch_qty >= row.qty: + item['batch_no'] = batch_no + item_details.batch_no[batch_no] -= row.qty + break + + items.append(item) + + ste_dict = make_rm_stock_entry(args.po_no, items) + doc = frappe.get_doc(ste_dict) + doc.insert() + doc.submit() + + return doc + +def make_subcontract_items(): + sub_contracted_items = {'Subcontracted Item SA1': {}, 'Subcontracted Item SA2': {}, 'Subcontracted Item SA3': {}, + 'Subcontracted Item SA4': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'SBAT.####'}, + 'Subcontracted Item SA5': {}, 'Subcontracted Item SA6': {}} + + for item, properties in sub_contracted_items.items(): + if not frappe.db.exists('Item', item): + properties.update({'is_stock_item': 1, 'is_sub_contracted_item': 1}) + make_item(item, properties) + +def make_raw_materials(): + raw_materials = {'Subcontracted SRM Item 1': {}, + 'Subcontracted SRM Item 2': {'has_serial_no': 1, 'serial_no_series': 'SRI.####'}, + 'Subcontracted SRM Item 3': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'BAT.####'}, + 'Subcontracted SRM Item 4': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}, + 'Subcontracted SRM Item 5': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}} + + for item, properties in raw_materials.items(): + if not frappe.db.exists('Item', item): + properties.update({'is_stock_item': 1}) + make_item(item, properties) + +def make_bom_for_subcontracted_items(): + boms = { + 'Subcontracted Item SA1': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], + 'Subcontracted Item SA2': ['Subcontracted SRM Item 2'], + 'Subcontracted Item SA3': ['Subcontracted SRM Item 2'], + 'Subcontracted Item SA4': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], + 'Subcontracted Item SA5': ['Subcontracted SRM Item 5'], + 'Subcontracted Item SA6': ['Subcontracted SRM Item 3'] + } + + for item_code, raw_materials in boms.items(): + if not frappe.db.exists('BOM', {'item': item_code}): + make_bom(item=item_code, raw_materials=raw_materials, rate=100) + +def set_backflush_based_on(based_on): + frappe.db.set_value('Buying Settings', None, + 'backflush_raw_materials_of_subcontract_based_on', based_on) \ No newline at end of file From 9a2db0b5b196597802ddd79119108a1b4615c3b0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 1 Jun 2021 11:24:01 +0530 Subject: [PATCH 131/376] fix: semgrep error --- .../subcontract_order_summary.py | 28 ++++++++----------- erpnext/controllers/subcontracting.py | 6 ++-- .../stock/doctype/stock_entry/stock_entry.py | 3 +- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 8b08d2a2840..0b14e119ab8 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -104,26 +104,26 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 140 + "width": 160 }, { - "label": _("Qty"), + "label": _("Order Qty"), "fieldname": "qty", "fieldtype": "Float", - "width": 70 + "width": 90 }, { - "label": _("Received"), + "label": _("Received Qty"), "fieldname": "received_qty", "fieldtype": "Float", - "width": 80 + "width": 110 }, { "label": _("Supplied Item"), "fieldname": "rm_item_code", "fieldtype": "Link", "options": "Item", - "width": 140 + "width": 160 }, { "label": _("Required Qty"), @@ -138,21 +138,15 @@ def get_columns(): "width": 110 }, { - "label": _("Returned Qty"), - "fieldname": "returned_qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Total Supplied"), - "fieldname": "total_supplied_qty", + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120 }, { - "label": _("Consumed Qty"), - "fieldname": "consumed_qty", + "label": _("Returned Qty"), + "fieldname": "returned_qty", "fieldtype": "Float", "width": 110 - }, + } ] \ No newline at end of file diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index fe775766da5..a9a38bd02da 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -101,9 +101,9 @@ class Subcontracting(object): self.set_alternative_item_details(row) for doctype in ['Purchase Receipt', 'Purchase Invoice']: - self.remove_consumed_materials(doctype) + self.update_consumed_materials(doctype) - def remove_consumed_materials(self, doctype, return_consumed_items=False): + def update_consumed_materials(self, doctype, return_consumed_items=False): '''Deduct the consumed materials from the available materials.''' pr_items = self.get_received_items(doctype) @@ -311,7 +311,7 @@ class Subcontracting(object): return self.get_purchase_orders() - consumed_items, pr_items = self.remove_consumed_materials(self.doctype, return_consumed_items=True) + consumed_items, pr_items = self.update_consumed_materials(self.doctype, return_consumed_items=True) itemwise_consumed_qty = defaultdict(float) for row in consumed_items: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 213280870af..0009926f5d6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1291,7 +1291,8 @@ class StockEntry(StockController): item_dict[item]["qty"] = 0 # delete items with 0 qty - for item in item_dict.keys(): + list_of_items = item_dict.keys() + for item in list_of_items: if not item_dict[item]["qty"]: del item_dict[item] From 2fb5291785acf9b1e33f2cf8ebe6a4fa2c9ab887 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 7 Jun 2021 21:20:33 +0530 Subject: [PATCH 132/376] fix: toggle consumed qty field based on condition --- .../doctype/purchase_receipt/purchase_receipt.js | 11 +++++++++-- .../doctype/purchase_receipt/purchase_receipt.py | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 887b15a2115..cac6bf884b9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -41,8 +41,6 @@ frappe.ui.form.on("Purchase Receipt", { } }); - frm.set_df_property('supplied_items', 'cannot_add_rows', 1); - }, onload: function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { @@ -77,6 +75,15 @@ frappe.ui.form.on("Purchase Receipt", { } frm.events.add_custom_buttons(frm); + frm.trigger('toggle_subcontracting_fields'); + }, + + toggle_subcontracting_fields: function(frm) { + frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', + 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); + + frm.set_df_property('supplied_items', 'cannot_add_rows', 1); + frm.set_df_property('supplied_items', 'cannot_delete_rows', 1); }, add_custom_buttons: function(frm) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b8580f95a3a..264561f376e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -102,6 +102,11 @@ class PurchaseReceipt(BuyingController): if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): apply_putaway_rule(self.doctype, self.get("items"), self.company) + def onload(self): + super(PurchaseReceipt, self).onload() + self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', + 'backflush_raw_materials_of_subcontract_based_on')) + def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() From ddb0ec261af7470ac2b07f103b24d9386b7b111e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 8 Jun 2021 10:36:39 +0530 Subject: [PATCH 133/376] fix: code cleanup and convert public method to private for subcontracting class --- .../doctype/purchase_order/purchase_order.js | 5 +- .../doctype/purchase_order/purchase_order.py | 13 ++- .../purchase_order_item_supplied.json | 6 +- .../subcontract_order_summary.py | 2 +- erpnext/controllers/subcontracting.py | 106 +++++++++--------- erpnext/public/js/controllers/transaction.js | 4 + .../stock/doctype/stock_entry/stock_entry.py | 10 +- 7 files changed, 76 insertions(+), 70 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 440cde6d9e2..233a9c87e59 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -74,6 +74,7 @@ frappe.ui.form.on("Purchase Order", { frm.add_custom_button(__('Return of Components'), () => { frm.call({ method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier', + freeze: true, freeze_message: __('Creating Stock Entry'), args: { purchase_order: frm.doc.name, po_details: po_details }, callback: function(r) { @@ -545,14 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( ], primary_action: function() { var data = d.get_values(); - var content_msg = 'Reason for hold: ' + data.reason_for_hold; + let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold; frappe.call({ method: "frappe.desk.form.utils.add_comment", args: { reference_doctype: me.frm.doctype, reference_name: me.frm.docname, - content: __(content_msg), + content: __(reason_for_hold), comment_email: frappe.session.user, comment_by: frappe.session.user_fullname }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 724f863e0f7..eaa502ff7f0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status from erpnext.stock.utils import get_bin from erpnext.accounts.party import get_party_account_currency -from six import string_types from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details -from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ - unlink_inter_company_doc +from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party, + update_linked_doc, unlink_inter_company_doc) form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -504,7 +503,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions @frappe.whitelist() def make_rm_stock_entry(purchase_order, rm_items): rm_items_list = rm_items - if isinstance(rm_items, string_types): + + if isinstance(rm_items, str): rm_items_list = json.loads(rm_items) elif not rm_items: frappe.throw(_("No Items available for transfer")) @@ -588,7 +588,7 @@ def make_inter_company_sales_order(source_name, target_doc=None): @frappe.whitelist() def get_materials_from_supplier(purchase_order, po_details): - if isinstance(po_details, string_types): + if isinstance(po_details, str): po_details = json.loads(po_details) doc = frappe.get_cached_doc('Purchase Order', purchase_order) @@ -615,7 +615,8 @@ def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_deta if value.batch_no: for batch_no, qty in value.batch_no.items(): - add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) + if qty > 0: + add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) else: add_items_in_ste(ste_doc, value, value.qty, po_details) diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index 505ecd84c51..60247bd90bb 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -22,9 +22,9 @@ "required_qty", "supplied_qty", "col_break1", + "consumed_qty", "returned_qty", - "total_supplied_qty", - "consumed_qty" + "total_supplied_qty" ], "fields": [ { @@ -183,7 +183,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-01 00:41:54.123436", + "modified": "2021-06-09 15:17:58.128242", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 0b14e119ab8..0c0d4f0531d 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -87,7 +87,7 @@ def get_subcontracted_data(po_details, data): def get_columns(): return [ { - "label": _("Id"), + "label": _("Purchase Order"), "fieldname": "po_id", "fieldtype": "Link", "options": "Purchase Order", diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index a9a38bd02da..e81c0f5732b 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -1,39 +1,37 @@ -from __future__ import unicode_literals - import frappe from frappe import _ from frappe.utils import flt, cint from collections import defaultdict from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos -class Subcontracting(object): +class Subcontracting(): def set_materials_for_subcontracted_items(self, raw_material_table): if self.doctype == 'Purchase Invoice' and not self.update_stock: return self.raw_material_table = raw_material_table - self.identify_change_in_item_table() - self.prepare_supplied_items() - self.validate_consumed_qty() + self.__identify_change_in_item_table() + self.__prepare_supplied_items() + self.__validate_consumed_qty() - def prepare_supplied_items(self): + def __prepare_supplied_items(self): self.initialized_fields() - self.get_purchase_orders() - self.get_pending_qty_to_receive() + self.__get_purchase_orders() + self.__get_pending_qty_to_receive() self.get_available_materials() - self.remove_changed_rows() - self.set_supplied_items() + self.__remove_changed_rows() + self.__set_supplied_items() def initialized_fields(self): self.available_materials = frappe._dict() self.alternative_item_details = frappe._dict() - self.get_backflush_based_on() + self.__get_backflush_based_on() - def get_backflush_based_on(self): + def __get_backflush_based_on(self): self.backflush_based_on = frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") - def get_purchase_orders(self): + def __get_purchase_orders(self): self.purchase_orders = [] if self.doctype == 'Purchase Order': @@ -41,14 +39,14 @@ class Subcontracting(object): self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] - def identify_change_in_item_table(self): + def __identify_change_in_item_table(self): self.changed_name = [] if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table): self.set(self.raw_material_table, []) return - item_dict = self.get_data_before_save() + item_dict = self.__get_data_before_save() if not item_dict: return True @@ -61,7 +59,7 @@ class Subcontracting(object): self.changed_name.extend(item_dict.keys()) - def get_data_before_save(self): + def __get_data_before_save(self): item_dict = {} if self.doctype == 'Purchase Receipt' and self._doc_before_save: for row in self._doc_before_save.get('items'): @@ -80,7 +78,7 @@ class Subcontracting(object): if not self.purchase_orders: return - for row in self.get_transferred_items(): + for row in self.__get_transferred_items(): key = (row.rm_item_code, row.main_item_code, row.purchase_order) if key not in self.available_materials: @@ -98,20 +96,20 @@ class Subcontracting(object): if row.batch_no: details.batch_no[row.batch_no] += row.qty - self.set_alternative_item_details(row) + self.__set_alternative_item_details(row) for doctype in ['Purchase Receipt', 'Purchase Invoice']: - self.update_consumed_materials(doctype) + self.__update_consumed_materials(doctype) - def update_consumed_materials(self, doctype, return_consumed_items=False): + def __update_consumed_materials(self, doctype, return_consumed_items=False): '''Deduct the consumed materials from the available materials.''' - pr_items = self.get_received_items(doctype) + pr_items = self.__get_received_items(doctype) if not pr_items: return ([], {}) if return_consumed_items else None pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items} - consumed_materials = self.get_consumed_items(doctype, pr_items.keys()) + consumed_materials = self.__get_consumed_items(doctype, pr_items.keys()) if return_consumed_items: return (consumed_materials, pr_items) @@ -130,7 +128,7 @@ class Subcontracting(object): if row.batch_no: self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty - def get_transferred_items(self): + def __get_transferred_items(self): fields = ['`tabStock Entry`.`purchase_order`'] alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'} @@ -149,7 +147,7 @@ class Subcontracting(object): return frappe.get_all('Stock Entry', fields = fields, filters=filters) - def get_received_items(self, doctype): + def __get_received_items(self, doctype): fields = [] self.po_field = 'purchase_order' if doctype == 'Purchase Receipt' else 'po_detail' @@ -162,16 +160,16 @@ class Subcontracting(object): return frappe.get_all(f'{doctype}', fields = fields, filters = filters) - def get_consumed_items(self, doctype, pr_items): + def __get_consumed_items(self, doctype, pr_items): return frappe.get_all(f'{doctype} Item Supplied', fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items))}) - def set_alternative_item_details(self, row): + def __set_alternative_item_details(self, row): if row.get('original_item'): self.alternative_item_details[row.get('original_item')] = row - def get_pending_qty_to_receive(self): + def __get_pending_qty_to_receive(self): '''Get qty to be received against the purchase order.''' self.qty_to_be_received = defaultdict(float) @@ -183,7 +181,7 @@ class Subcontracting(object): self.qty_to_be_received[(row.item_code, row.parent)] += row.qty - def get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item' fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit'] @@ -197,7 +195,7 @@ class Subcontracting(object): return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] - def remove_changed_rows(self): + def __remove_changed_rows(self): if not self.changed_name: return @@ -212,7 +210,7 @@ class Subcontracting(object): i += 1 - def set_supplied_items(self): + def __set_supplied_items(self): self.bom_items = {} has_supplied_items = True if self.get(self.raw_material_table) else False @@ -222,28 +220,28 @@ class Subcontracting(object): continue if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': - for bom_item in self.get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): + for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor) bom_item.main_item_code = row.item_code - self.update_reserve_warehouse(bom_item, row) - self.set_alternative_item(bom_item) - self.add_supplied_item(row, bom_item, qty) + self.__update_reserve_warehouse(bom_item, row) + self.__set_alternative_item(bom_item) + self.__add_supplied_item(row, bom_item, qty) elif self.backflush_based_on != 'BOM': for key, transfer_item in self.available_materials.items(): if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0: - qty = self.get_qty_based_on_material_transfer(row, transfer_item) or 0 + qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 transfer_item.qty -= qty - self.add_supplied_item(row, transfer_item.get('item_details'), qty) + self.__add_supplied_item(row, transfer_item.get('item_details'), qty) if self.qty_to_be_received: self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty - def update_reserve_warehouse(self, row, item): + def __update_reserve_warehouse(self, row, item): if self.doctype == 'Purchase Order': row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse) - def get_qty_based_on_material_transfer(self, item_row, transfer_item): + def __get_qty_based_on_material_transfer(self, item_row, transfer_item): key = (item_row.item_code, item_row.purchase_order) if self.qty_to_be_received == item_row.qty: @@ -257,11 +255,11 @@ class Subcontracting(object): return qty - def set_alternative_item(self, bom_item): + def __set_alternative_item(self, bom_item): if self.alternative_item_details.get(bom_item.rm_item_code): bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) - def add_supplied_item(self, item_row, bom_item, qty): + def __add_supplied_item(self, item_row, bom_item, qty): bom_item.conversion_factor = item_row.conversion_factor rm_obj = self.append(self.raw_material_table, bom_item) rm_obj.reference_name = item_row.name @@ -269,15 +267,15 @@ class Subcontracting(object): if self.doctype == 'Purchase Order': rm_obj.required_qty = qty else: - self.set_batch_nos(bom_item, item_row, rm_obj, qty) + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) - def set_batch_nos(self, bom_item, item_row, rm_obj, qty): + def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): if batch_qty >= qty: - self.set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) self.available_materials[key]['batch_no'][batch_no] -= qty return @@ -285,18 +283,18 @@ class Subcontracting(object): qty -= batch_qty new_rm_obj = self.append(self.raw_material_table, bom_item) new_rm_obj.reference_name = item_row.name - self.set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) + self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) self.available_materials[key]['batch_no'][batch_no] = 0 else: rm_obj.required_qty = qty rm_obj.consumed_qty = qty - self.set_serial_nos(item_row, rm_obj) + self.__set_serial_nos(item_row, rm_obj) - def set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, 'required_qty': qty}) - self.set_serial_nos(item_row, rm_obj) + self.__set_serial_nos(item_row, rm_obj) - def set_serial_nos(self, item_row, rm_obj): + def __set_serial_nos(self, item_row, rm_obj): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) if (self.available_materials.get(key) and self.available_materials[key]['serial_no']): used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)] @@ -310,17 +308,17 @@ class Subcontracting(object): if self.is_subcontracted != 'Yes': return - self.get_purchase_orders() - consumed_items, pr_items = self.update_consumed_materials(self.doctype, return_consumed_items=True) + self.__get_purchase_orders() + consumed_items, pr_items = self.__update_consumed_materials(self.doctype, return_consumed_items=True) itemwise_consumed_qty = defaultdict(float) for row in consumed_items: key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) itemwise_consumed_qty[key] += row.consumed_qty - self.update_consumed_qty_in_po(itemwise_consumed_qty) + self.__update_consumed_qty_in_po(itemwise_consumed_qty) - def update_consumed_qty_in_po(self, itemwise_consumed_qty): + def __update_consumed_qty_in_po(self, itemwise_consumed_qty): fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name'] filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)} @@ -334,7 +332,7 @@ class Subcontracting(object): itemwise_consumed_qty[key] -= consumed_qty frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) - def validate_consumed_qty(self): + def __validate_consumed_qty(self): for row in self.get(self.raw_material_table): if flt(row.consumed_qty) == 0.0 and row.get('serial_no'): msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 89fed3bf0dc..978c8f48793 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -723,6 +723,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var me = this; var item = frappe.get_doc(cdt, cdn); + if (item && item.doctype === 'Purchase Receipt Item Supplied') { + return; + } + if (item && item.serial_no) { if (!item.item_code) { this.frm.trigger("item_code", cdt, cdn); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0009926f5d6..66f8b63cb92 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1005,10 +1005,12 @@ class StockEntry(StockController): if self.purchase_order and self.purpose == "Send to Subcontractor": #Get PO Supplied Items Details item_wh = frappe._dict(frappe.db.sql(""" - select rm_item_code, reserve_warehouse - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup - where po.name = poitemsup.parent - and po.name = %s""",self.purchase_order)) + SELECT + rm_item_code, reserve_warehouse + FROM + `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup + WHERE + po.name = poitemsup.parent and po.name = %s """,self.purchase_order)) for item in itervalues(item_dict): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): From 5cc3f14506bb75fd525eb774cc5d70a5f4204947 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 15 Jun 2021 17:29:52 +0530 Subject: [PATCH 134/376] fix: purchase invoice qty change not recalculate the consumed qty and added test cases for purchase invoice --- .../purchase_invoice/purchase_invoice.json | 617 +++++------------- .../purchase_invoice/purchase_invoice.py | 2 + erpnext/controllers/buying_controller.py | 5 + erpnext/controllers/subcontracting.py | 41 +- erpnext/public/js/controllers/buying.js | 11 + .../purchase_receipt/purchase_receipt.js | 9 - .../purchase_receipt/purchase_receipt.py | 5 - erpnext/tests/test_subcontracting.py | 267 +++++++- 8 files changed, 461 insertions(+), 496 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index a714ac78274..00ef7d5c184 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -175,9 +175,7 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "naming_series", @@ -189,9 +187,7 @@ "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1, - "show_days": 1, - "show_seconds": 1 + "set_only_once": 1 }, { "fieldname": "supplier", @@ -203,9 +199,7 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "bold": 1, @@ -217,9 +211,7 @@ "label": "Supplier Name", "oldfieldname": "supplier_name", "oldfieldtype": "Data", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fetch_from": "supplier.tax_id", @@ -227,27 +219,21 @@ "fieldtype": "Read Only", "label": "Tax Id", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date", "oldfieldname": "due_date", - "oldfieldtype": "Date", - "show_days": 1, - "show_seconds": 1 + "oldfieldtype": "Date" }, { "default": "0", "fieldname": "is_paid", "fieldtype": "Check", "label": "Is Paid", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", @@ -255,25 +241,19 @@ "fieldtype": "Check", "label": "Is Return (Debit Note)", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -283,17 +263,13 @@ "label": "Company", "options": "Company", "print_hide": 1, - "remember_last_selected_value": 1, - "show_days": 1, - "show_seconds": 1 + "remember_last_selected_value": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center", - "show_days": 1, - "show_seconds": 1 + "options": "Cost Center" }, { "default": "Today", @@ -305,9 +281,7 @@ "oldfieldtype": "Date", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "posting_time", @@ -316,8 +290,6 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { @@ -326,9 +298,7 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "amended_from", @@ -340,58 +310,44 @@ "oldfieldtype": "Link", "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", - "label": "Hold Invoice", - "show_days": 1, - "show_seconds": 1 + "label": "Hold Invoice" }, { "default": "0", "fieldname": "on_hold", "fieldtype": "Check", - "label": "Hold Invoice", - "show_days": 1, - "show_seconds": 1 + "label": "Hold Invoice" }, { "depends_on": "eval:doc.on_hold", "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date", - "show_days": 1, - "show_seconds": 1 + "label": "Release Date" }, { "fieldname": "cb_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "eval:doc.on_hold", "fieldname": "hold_comment", "fieldtype": "Small Text", - "label": "Reason For Putting On Hold", - "show_days": 1, - "show_seconds": 1 + "label": "Reason For Putting On Hold" }, { "collapsible": 1, "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details", - "show_days": 1, - "show_seconds": 1 + "label": "Supplier Invoice Details" }, { "fieldname": "bill_no", @@ -399,15 +355,11 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "bill_date", @@ -416,17 +368,13 @@ "no_copy": 1, "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", - "label": "Returns", - "show_days": 1, - "show_seconds": 1 + "label": "Returns" }, { "depends_on": "return_against", @@ -436,34 +384,26 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact", - "show_days": 1, - "show_seconds": 1 + "label": "Address and Contact" }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_person", @@ -471,67 +411,51 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_email", "fieldtype": "Small Text", "label": "Contact Email", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "fieldname": "currency", @@ -540,9 +464,7 @@ "oldfieldname": "currency", "oldfieldtype": "Select", "options": "Currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "conversion_rate", @@ -551,24 +473,18 @@ "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", "precision": "9", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break2", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "price_list_currency", @@ -576,18 +492,14 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", @@ -596,15 +508,11 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "depends_on": "update_stock", @@ -613,9 +521,7 @@ "fieldtype": "Link", "label": "Set Accepted Warehouse", "options": "Warehouse", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "update_stock", @@ -625,15 +531,11 @@ "label": "Rejected Warehouse", "no_copy": 1, "options": "Warehouse", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "No", @@ -641,33 +543,25 @@ "fieldtype": "Select", "label": "Raw Materials Supplied", "options": "No\nYes", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-shopping-cart" }, { "default": "0", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode", - "show_days": 1, - "show_seconds": 1 + "label": "Scan Barcode" }, { "allow_bulk_edit": 1, @@ -677,56 +571,43 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Purchase Invoice Item", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules", - "show_days": 1, - "show_seconds": 1 + "label": "Pricing Rules" }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", - "label": "Raw Materials Supplied", - "show_days": 1, - "show_seconds": 1 + "label": "Raw Materials Supplied" }, { + "depends_on": "update_stock", "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", - "options": "Purchase Receipt Item Supplied", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "options": "Purchase Receipt Item Supplied" }, { "fieldname": "section_break_26", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_total", @@ -734,9 +615,7 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_net_total", @@ -746,24 +625,18 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_28", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "net_total", @@ -773,56 +646,42 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-money" }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_49", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "section_break_51", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "taxes_and_charges", @@ -831,9 +690,7 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "taxes", @@ -841,17 +698,13 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges", - "show_days": 1, - "show_seconds": 1 + "options": "Purchase Taxes and Charges" }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup", - "show_days": 1, - "show_seconds": 1 + "label": "Tax Breakup" }, { "fieldname": "other_charges_calculation", @@ -860,17 +713,13 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-money" }, { "fieldname": "base_taxes_and_charges_added", @@ -880,9 +729,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -892,9 +739,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -904,15 +749,11 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_40", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "taxes_and_charges_added", @@ -922,9 +763,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -934,9 +773,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_taxes_and_charges", @@ -944,18 +781,14 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount", - "show_days": 1, - "show_seconds": 1 + "label": "Additional Discount" }, { "default": "Grand Total", @@ -963,9 +796,7 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_discount_amount", @@ -973,38 +804,28 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "section_break_49", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "base_grand_total", @@ -1014,9 +835,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1026,9 +845,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1038,9 +855,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_in_words", @@ -1050,17 +865,13 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break8", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -1071,9 +882,7 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1083,9 +892,7 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1095,9 +902,7 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "in_words", @@ -1107,9 +912,7 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_advance", @@ -1120,9 +923,7 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "outstanding_amount", @@ -1133,18 +934,14 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total", - "show_days": 1, - "show_seconds": 1 + "label": "Disable Rounded Total" }, { "collapsible": 1, @@ -1152,26 +949,20 @@ "depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", - "label": "Payments", - "show_days": 1, - "show_seconds": 1 + "label": "Payments" }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "fieldname": "clearance_date", @@ -1179,15 +970,11 @@ "label": "Clearance Date", "no_copy": 1, "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_br_payments", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "is_paid", @@ -1196,9 +983,7 @@ "label": "Paid Amount", "no_copy": 1, "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_paid_amount", @@ -1207,9 +992,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, @@ -1217,9 +1000,7 @@ "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", - "label": "Write Off", - "show_days": 1, - "show_seconds": 1 + "label": "Write Off" }, { "fieldname": "write_off_amount", @@ -1227,9 +1008,7 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_write_off_amount", @@ -1238,15 +1017,11 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_61", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1254,9 +1029,7 @@ "fieldtype": "Link", "label": "Write Off Account", "options": "Account", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1264,9 +1037,7 @@ "fieldtype": "Link", "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -1276,17 +1047,13 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", - "label": "Set Advances and Allocate (FIFO)", - "show_days": 1, - "show_seconds": 1 + "label": "Set Advances and Allocate (FIFO)" }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1294,9 +1061,7 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "advances", @@ -1306,26 +1071,20 @@ "oldfieldname": "advance_allocation_details", "oldfieldtype": "Table", "options": "Purchase Invoice Advance", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms", - "show_days": 1, - "show_seconds": 1 + "label": "Payment Terms" }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", - "options": "Payment Terms Template", - "show_days": 1, - "show_seconds": 1 + "options": "Payment Terms Template" }, { "fieldname": "payment_schedule", @@ -1333,9 +1092,7 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -1343,33 +1100,25 @@ "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", - "options": "fa fa-legal", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-legal" }, { "fieldname": "tc_name", "fieldtype": "Link", "label": "Terms", "options": "Terms and Conditions", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1", - "show_days": 1, - "show_seconds": 1 + "label": "Terms and Conditions1" }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings", - "show_days": 1, - "show_seconds": 1 + "label": "Printing Settings" }, { "allow_on_submit": 1, @@ -1377,9 +1126,7 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1387,15 +1134,11 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_112", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "allow_on_submit": 1, @@ -1407,18 +1150,14 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1, - "show_days": 1, - "show_seconds": 1 + "report_hide": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, @@ -1427,9 +1166,7 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "credit_to", @@ -1440,9 +1177,7 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "party_account_currency", @@ -1452,9 +1187,7 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "No", @@ -1464,9 +1197,7 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "against_expense_account", @@ -1476,15 +1207,11 @@ "no_copy": 1, "oldfieldname": "against_expense_account", "oldfieldtype": "Small Text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_63", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "Draft", @@ -1493,9 +1220,7 @@ "in_standard_filter": 1, "label": "Status", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "inter_company_invoice_reference", @@ -1504,9 +1229,7 @@ "no_copy": 1, "options": "Sales Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "remarks", @@ -1515,18 +1238,14 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1535,9 +1254,7 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1546,15 +1263,11 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_114", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "auto_repeat", @@ -1563,32 +1276,24 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference", - "show_days": 1, - "show_seconds": 1 + "label": "Update Auto Repeat Reference" }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions ", - "show_days": 1, - "show_seconds": 1 + "label": "Accounting Dimensions " }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "0", @@ -1596,9 +1301,7 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "tax_withholding_category", @@ -1606,25 +1309,19 @@ "hidden": 1, "label": "Tax Withholding Category", "options": "Tax Withholding Category", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "billing_address", "fieldtype": "Link", "label": "Select Billing Address", - "options": "Address", - "show_days": 1, - "show_seconds": 1 + "options": "Address" }, { "fieldname": "billing_address_display", "fieldtype": "Small Text", "label": "Billing Address", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "project", @@ -1638,9 +1335,7 @@ "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "depends_on": "eval:doc.is_internal_supplier", @@ -1649,9 +1344,7 @@ "fieldname": "represents_company", "fieldtype": "Link", "label": "Represents Company", - "options": "Company", - "show_days": 1, - "show_seconds": 1 + "options": "Company" }, { "depends_on": "eval:doc.update_stock && doc.is_internal_supplier", @@ -1663,8 +1356,6 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", - "show_days": 1, - "show_seconds": 1, "width": "50px" }, { @@ -1692,7 +1383,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-06-09 12:30:25.632109", + "modified": "2021-06-15 18:20:56.806195", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0ee0bc7e11f..45d89ad1c87 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController): # because updating ordered qty in bin depends upon updated ordered qty in PO if self.update_stock == 1: self.update_stock_ledger() + self.set_consumed_qty_in_po() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") @@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.update_stock_ledger() self.delete_auto_created_batches() + self.set_consumed_qty_in_po() self.make_gl_entries_on_cancel() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 1907885717b..0b0da5f413b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -58,6 +58,11 @@ class BuyingController(StockController, Subcontracting): if self.doctype in ("Purchase Receipt", "Purchase Invoice"): self.update_valuation_rate() + def onload(self): + super(BuyingController, self).onload() + self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', + 'backflush_raw_materials_of_subcontract_based_on')) + def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index e81c0f5732b..db841626a59 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -40,9 +40,10 @@ class Subcontracting(): self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] def __identify_change_in_item_table(self): - self.changed_name = [] + self.__changed_name = [] + self.__reference_name = [] - if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table): + if self.doctype == 'Purchase Order' or self.is_new(): self.set(self.raw_material_table, []) return @@ -51,17 +52,18 @@ class Subcontracting(): return True for n_row in self.items: + self.__reference_name.append(n_row.name) if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]: - self.changed_name.append(n_row.name) + self.__changed_name.append(n_row.name) if item_dict.get(n_row.name): del item_dict[n_row.name] - self.changed_name.extend(item_dict.keys()) + self.__changed_name.extend(item_dict.keys()) def __get_data_before_save(self): item_dict = {} - if self.doctype == 'Purchase Receipt' and self._doc_before_save: + if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save: for row in self._doc_before_save.get('items'): item_dict[row.name] = (row.item_code, row.qty) @@ -149,7 +151,7 @@ class Subcontracting(): def __get_received_items(self, doctype): fields = [] - self.po_field = 'purchase_order' if doctype == 'Purchase Receipt' else 'po_detail' + self.po_field = 'purchase_order' for field in ['name', self.po_field, 'parent']: fields.append(f'`tab{doctype} Item`.`{field}`') @@ -161,9 +163,9 @@ class Subcontracting(): return frappe.get_all(f'{doctype}', fields = fields, filters = filters) def __get_consumed_items(self, doctype, pr_items): - return frappe.get_all(f'{doctype} Item Supplied', + return frappe.get_all('Purchase Receipt Item Supplied', fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], - filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items))}) + filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype}) def __set_alternative_item_details(self, row): if row.get('original_item'): @@ -196,13 +198,16 @@ class Subcontracting(): return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] def __remove_changed_rows(self): - if not self.changed_name: + if not self.__changed_name: return i=1 self.set(self.raw_material_table, []) for d in self._doc_before_save.supplied_items: - if d.reference_name in self.changed_name: + if d.reference_name in self.__changed_name: + continue + + if (d.reference_name not in self.__reference_name): continue d.idx = i @@ -215,8 +220,8 @@ class Subcontracting(): has_supplied_items = True if self.get(self.raw_material_table) else False for row in self.items: - if (self.doctype != 'Purchase Order' and ((self.changed_name and row.name not in self.changed_name) - or (has_supplied_items and not self.changed_name))): + if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name) + or (has_supplied_items and not self.__changed_name))): continue if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': @@ -305,16 +310,18 @@ class Subcontracting(): self.available_materials[key]['serial_no'].remove(sn) def set_consumed_qty_in_po(self): + # Update consumed qty back in the purchase order if self.is_subcontracted != 'Yes': return self.__get_purchase_orders() - consumed_items, pr_items = self.__update_consumed_materials(self.doctype, return_consumed_items=True) - itemwise_consumed_qty = defaultdict(float) - for row in consumed_items: - key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) - itemwise_consumed_qty[key] += row.consumed_qty + for doctype in ['Purchase Receipt', 'Purchase Invoice']: + consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True) + + for row in consumed_items: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + itemwise_consumed_qty[key] += row.consumed_qty self.__update_consumed_qty_in_po(itemwise_consumed_qty) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index e7dcd410682..5c9f5d7da43 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -122,9 +122,20 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this.set_from_product_bundle(); } + this.toggle_subcontracting_fields(); this._super(); }, + toggle_subcontracting_fields: function() { + if (in_list(['Purchase Receipt', 'Purchase Invoice'], this.frm.doc.doctype)) { + this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', + 'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM'); + + this.frm.set_df_property('supplied_items', 'cannot_add_rows', 1); + this.frm.set_df_property('supplied_items', 'cannot_delete_rows', 1); + } + }, + supplier: function() { var me = this; erpnext.utils.get_party_details(this.frm, null, null, function(){ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index cac6bf884b9..befdad96924 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -75,15 +75,6 @@ frappe.ui.form.on("Purchase Receipt", { } frm.events.add_custom_buttons(frm); - frm.trigger('toggle_subcontracting_fields'); - }, - - toggle_subcontracting_fields: function(frm) { - frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', - 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); - - frm.set_df_property('supplied_items', 'cannot_add_rows', 1); - frm.set_df_property('supplied_items', 'cannot_delete_rows', 1); }, add_custom_buttons: function(frm) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 264561f376e..b8580f95a3a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -102,11 +102,6 @@ class PurchaseReceipt(BuyingController): if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): apply_putaway_rule(self.doctype, self.get("items"), self.company) - def onload(self): - super(PurchaseReceipt, self).onload() - self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', - 'backflush_raw_materials_of_subcontract_based_on')) - def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index c1a458a6ddb..d2438f8c607 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -10,7 +10,7 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.purchase_order import (make_rm_stock_entry, - make_purchase_receipt, get_materials_from_supplier) + make_purchase_receipt, make_purchase_invoice, get_materials_from_supplier) class TestSubcontracting(unittest.TestCase): def setUp(self): @@ -458,10 +458,273 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, details.qty) self.assertEqual(value.batch_no, details.batch_no) + + def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + self.assertEqual(value.qty, qty) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + + def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA2' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 5 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3])) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA6' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 5 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + + transferred_batch_no = '' + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + transferred_batch_no = details.batch_no + self.assertEqual(value.batch_no, details.batch_no) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0] + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + def test_item_with_batch_based_on_bom_for_purchase_invoice(self): + ''' + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + ''' + + set_backflush_based_on('BOM') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + def add_second_row_in_pr(pr): item_dict = {} for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom', - 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate']: + 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate', 'expense_account', 'po_detail']: item_dict[column] = pr.items[0].get(column) pr.append('items', item_dict) From f5db407461c1ee898a84d83587cf6f3eae3791c1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 18 Jun 2021 20:37:42 +0530 Subject: [PATCH 135/376] fix: available qty for consumption --- .../purchase_order/test_purchase_order.py | 3 - .../purchase_receipt_item_supplied.json | 20 ++++-- erpnext/controllers/buying_controller.py | 10 +-- erpnext/controllers/subcontracting.py | 66 ++++++++++++++++--- erpnext/stock/stock_ledger.py | 2 +- erpnext/tests/test_subcontracting.py | 31 +++++++++ 6 files changed, 110 insertions(+), 22 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 33d19714510..8563b97ab74 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -847,9 +847,6 @@ class TestPurchaseOrder(unittest.TestCase): for item in rm_items: transferred_rm_map[item.get('rm_item_code')] = item - for item in pr.get('supplied_items'): - self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty) - update_backflush_based_on("BOM") def test_supplied_qty_against_subcontracted_po(self): diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index d8c37f58819..f9cd72015a6 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -26,7 +26,8 @@ "secbreak_3", "batch_no", "col_break4", - "serial_no" + "serial_no", + "purchase_order" ], "fields": [ { @@ -81,9 +82,10 @@ "fieldname": "required_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", + "label": "Available Qty For Consumption", "oldfieldname": "required_qty", "oldfieldtype": "Currency", + "print_hide": 1, "read_only": 1 }, { @@ -91,7 +93,7 @@ "fieldname": "consumed_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Consumed Qty", + "label": "Qty to Be Consumed", "oldfieldname": "consumed_qty", "oldfieldtype": "Currency", "reqd": 1 @@ -190,12 +192,22 @@ "fieldtype": "Data", "label": "Item Name", "read_only": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "hidden": 1, + "label": "Purchase Order", + "no_copy": 1, + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-29 17:22:14.977117", + "modified": "2021-06-19 19:33:04.431213", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 0b0da5f413b..6a550e0e975 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -292,11 +292,13 @@ class BuyingController(StockController, Subcontracting): if item in self.sub_contracted_items and not item.bom: frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) - if self.doctype == "Purchase Order": - for supplied_item in self.get("supplied_items"): - if not supplied_item.reserve_warehouse: - frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code))) + if self.doctype != "Purchase Order": + return + for row in self.get("supplied_items"): + if not row.reserve_warehouse: + msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" + frappe.throw(_(msg)) else: for item in self.get("items"): if item.bom: diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index db841626a59..36ae1102164 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -1,6 +1,7 @@ import frappe +import copy from frappe import _ -from frappe.utils import flt, cint +from frappe.utils import flt, cint, get_link_to_form from collections import defaultdict from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -12,7 +13,7 @@ class Subcontracting(): self.raw_material_table = raw_material_table self.__identify_change_in_item_table() self.__prepare_supplied_items() - self.__validate_consumed_qty() + self.__validate_supplied_items() def __prepare_supplied_items(self): self.initialized_fields() @@ -24,6 +25,7 @@ class Subcontracting(): def initialized_fields(self): self.available_materials = frappe._dict() + self.__transferred_items = frappe._dict() self.alternative_item_details = frappe._dict() self.__get_backflush_based_on() @@ -100,6 +102,7 @@ class Subcontracting(): self.__set_alternative_item_details(row) + self.__transferred_items = copy.deepcopy(self.available_materials) for doctype in ['Purchase Receipt', 'Purchase Invoice']: self.__update_consumed_materials(doctype) @@ -254,6 +257,8 @@ class Subcontracting(): if self.qty_to_be_received: qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + transfer_item.item_details.required_qty = transfer_item.qty + if (transfer_item.serial_no or frappe.get_cached_value('UOM', transfer_item.item_details.stock_uom, 'must_be_whole_number')): return frappe.utils.ceil(qty) @@ -272,12 +277,15 @@ class Subcontracting(): if self.doctype == 'Purchase Order': rm_obj.required_qty = qty else: + rm_obj.consumed_qty = 0 + rm_obj.purchase_order = item_row.purchase_order self.__set_batch_nos(bom_item, item_row, rm_obj, qty) def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): + new_rm_obj = None for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): if batch_qty >= qty: self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) @@ -290,13 +298,21 @@ class Subcontracting(): new_rm_obj.reference_name = item_row.name self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) self.available_materials[key]['batch_no'][batch_no] = 0 + + if abs(qty) > 0 and not new_rm_obj: + self.__set_consumed_qty(rm_obj, qty) else: - rm_obj.required_qty = qty - rm_obj.consumed_qty = qty + self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) self.__set_serial_nos(item_row, rm_obj) + def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): + rm_obj.required_qty = required_qty + rm_obj.consumed_qty = consumed_qty + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): - rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, 'required_qty': qty}) + rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, + 'required_qty': qty, 'purchase_order': item_row.purchase_order}) + self.__set_serial_nos(item_row, rm_obj) def __set_serial_nos(self, item_row, rm_obj): @@ -339,9 +355,39 @@ class Subcontracting(): itemwise_consumed_qty[key] -= consumed_qty frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) - def __validate_consumed_qty(self): - for row in self.get(self.raw_material_table): - if flt(row.consumed_qty) == 0.0 and row.get('serial_no'): - msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + def __validate_supplied_items(self): + if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']: + return - frappe.throw(_(msg),title=_('Consumed Items Qty Check')) \ No newline at end of file + for row in self.get(self.raw_material_table): + self.__validate_consumed_qty(row) + + key = (row.rm_item_code, row.main_item_code, row.purchase_order) + if not self.__transferred_items or not self.__transferred_items.get(key): + return + + self.__validate_batch_no(row, key) + self.__validate_serial_no(row, key) + + def __validate_consumed_qty(self, row): + if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0: + msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + + frappe.throw(_(msg),title=_('Consumed Items Qty Check')) + + def __validate_batch_no(self, row, key): + if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'): + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) + + def __validate_serial_no(self, row, key): + if row.get('serial_no'): + serial_nos = get_serial_nos(row.get('serial_no')) + incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no')) + + if incorrect_sn: + incorrect_sn = "\n".join(incorrect_sn) + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) \ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fb2ecab2491..9fe89c3fa59 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -485,7 +485,7 @@ class update_entries_after(object): # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': - doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no) + doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) for d in (doc.items + doc.supplied_items): d.db_update() diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index d2438f8c607..8b0ce0957d4 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -395,6 +395,37 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, details.qty) self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + def test_incorrect_serial_no_components_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the serialized componenets to the supplier. + - Create purchase receipt and change the serial no which is not transferred. + - System should throw the error and not allowed to save the purchase receipt. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA2' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 10}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.save() + pr1.supplied_items[0].serial_no = 'ABCD' + self.assertRaises(frappe.ValidationError, pr1.save) + pr1.delete() + def test_partial_transfer_batch_based_on_material_transfer(self): ''' - Set backflush based on Material Transferred for Subcontract From 3d7f660bec36faf4c73dab15c7e6974e4473591f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 20 Jun 2021 10:20:35 +0530 Subject: [PATCH 136/376] fix: test case for Project Profitability report --- .../project_profitability/test_project_profitability.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index ea6bdb54ca3..180926fe258 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import getdate, nowdate +from frappe.utils import getdate, nowdate, add_days from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice @@ -16,17 +16,22 @@ class TestProjectProfitability(unittest.TestCase): make_salary_structure_for_timesheet(emp, company='_Test Company') self.timesheet = make_timesheet(emp, simulate = True, is_billable=1) self.salary_slip = make_salary_slip(self.timesheet.name) + holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate()) + if holidays: + frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1) + self.salary_slip.submit() self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') self.sales_invoice.due_date = nowdate() self.sales_invoice.submit() frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) + frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 0) def test_project_profitability(self): filters = { 'company': '_Test Company', - 'start_date': getdate(), + 'start_date': add_days(getdate(), -3), 'end_date': getdate() } From 9e616b09a2b40ae4c365b7cedcbb809ab587bf71 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 11 Jun 2021 17:27:08 +0530 Subject: [PATCH 137/376] fix: material request and supplier quotation not linked if sq created from supplier portal against rfq --- .../request_for_quotation.py | 22 ++++++++++--------- .../templates/includes/transaction_row.html | 8 ++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 0127eb81630..a4ce84e1cf9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items): create_rfq_items(sq_doc, supplier, data) def create_rfq_items(sq_doc, supplier, data): - sq_doc.append('items', { - "item_code": data.item_code, - "item_name": data.item_name, - "description": data.description, - "qty": data.qty, - "rate": data.rate, - "conversion_factor": data.conversion_factor if data.conversion_factor else None, - "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), - "warehouse": data.warehouse or '', + args = {} + + for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor', + 'warehouse', 'material_request', 'material_request_item', 'stock_qty']: + args[field] = data.get(field) + + args.update({ "request_for_quotation_item": data.name, - "request_for_quotation": data.parent + "request_for_quotation": data.parent, + "supplier_part_no": frappe.db.get_value("Item Supplier", + {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no") }) + sq_doc.append('items', args) + @frappe.whitelist() def get_pdf(doctype, name, supplier): doc = get_rfq_doc(doctype, name, supplier) diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html index 383413103e1..3cfb8d8440c 100644 --- a/erpnext/templates/includes/transaction_row.html +++ b/erpnext/templates/includes/transaction_row.html @@ -13,9 +13,11 @@ {{ doc.items_preview }} -
    - {{ doc.get_formatted("grand_total") }} -
    + {% if doc.get('grand_total') %} +
    + {{ doc.get_formatted("grand_total") }} +
    + {% endif %} Link From 740df95c581446991b8c338454f4dc9b6e61dcee Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sun, 20 Jun 2021 17:44:35 +0530 Subject: [PATCH 138/376] fix(Asset Repair): Set completion_date on changing repair_status to 'Completed' --- erpnext/assets/doctype/asset_repair/asset_repair.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index efa6a9d4949..ced3dad1e5f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -28,6 +28,10 @@ frappe.ui.form.on('Asset Repair', { } }); } + + if (frm.doc.repair_status == "Completed") { + frm.set_value('completion_date', frappe.datetime.now_datetime()); + } } }); From 582f18772632d98ab138390532aa52a836aa9ef3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jun 2021 00:59:02 +0530 Subject: [PATCH 139/376] fix: rate not able to change in purchase order --- erpnext/controllers/taxes_and_totals.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2bb83ea7f0f..56da5b71da0 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object): item.margin_type = None item.margin_rate_or_amount = 0.0 - if item.margin_type and item.margin_rate_or_amount: + if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate): + item.margin_type = "Amount" + item.margin_rate_or_amount = flt(item.rate - item.price_list_rate, + item.precision("margin_rate_or_amount")) + item.rate_with_margin = item.rate + + elif item.margin_type and item.margin_rate_or_amount: margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 rate_with_margin = flt(item.price_list_rate) + flt(margin_value) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) From fb89008a13a49e57825228bd8a179c9e8e963aa2 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 21 Jun 2021 10:49:09 +0530 Subject: [PATCH 140/376] fix(pos): unsupported operand type -=: for 'float' and 'NoneType' (#26097) --- .../accounts/doctype/accounts_settings/accounts_settings.json | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/public/js/controllers/transaction.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 0ff7230e554..703e93c0757 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -260,7 +260,7 @@ "description": "If enabled, ledger entries will be posted for change amount in POS transactions", "fieldname": "post_change_gl_entries", "fieldtype": "Check", - "label": "Change Ledger Entries for Change Amount" + "label": "Create Ledger Entries for Change Amount" } ], "icon": "icon-cog", @@ -268,7 +268,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-16 13:14:45.739107", + "modified": "2021-06-17 20:26:03.721202", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e14f305fc55..55a5b99907b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -989,7 +989,7 @@ class SalesInvoice(SellingController): for payment_mode in self.payments: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: - payment_mode.base_amount -= self.change_amount + payment_mode.base_amount -= flt(self.change_amount) if payment_mode.amount: # POS, make payment entries diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 978c8f48793..6dc40f05e7a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -387,7 +387,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(this.frm.doc.scan_barcode) { frappe.call({ - method: "erpnext.selling.page.point_of_sale.point_of_sale.search_serial_or_batch_or_barcode_number", + method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number", args: { search_value: this.frm.doc.scan_barcode } }).then(r => { const data = r && r.message; From e78364c1917e0eeccd45587221fc51f00e185586 Mon Sep 17 00:00:00 2001 From: Ankush Date: Mon, 21 Jun 2021 11:15:16 +0530 Subject: [PATCH 142/376] fix: status indicator for delivery notes (#26062) On list view `per_returned` isn't fetched i.e. `undefined` which become 0 hence the list view indicator is false. This "computation" is already done by status updater, so relying on doc.status is better than redefining it. --- erpnext/stock/doctype/delivery_note/delivery_note_list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index f08125b199d..04028980473 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -6,8 +6,8 @@ frappe.listview_settings['Delivery Note'] = { return [__("Return"), "gray", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; - } else if (flt(doc.per_returned, 2) === 100) { - return [__("Return Issued"), "grey", "per_returned,=,100"]; + } else if (doc.status === "Return Issued") { + return [__("Return Issued"), "grey", "status,=,Return Issued"]; } else if (flt(doc.per_billed, 2) < 100) { return [__("To Bill"), "orange", "per_billed,<,100"]; } else if (flt(doc.per_billed, 2) === 100) { From 9b54aee43e188b321afbeebee0c9a8bf377e5b68 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Jun 2021 11:18:56 +0530 Subject: [PATCH 143/376] test: service item purchase with perpetual inventory enabled --- .../purchase_receipt/test_purchase_receipt.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 95096d77d77..33250f9c889 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1004,6 +1004,47 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) + def test_service_item_purchase_with_perpetual_inventory(self): + company = '_Test Company with perpetual inventory' + service_item = '_Test Non Stock Item' + + before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items') + frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1) + srbnb_account = 'Stock Received But Not Billed - TCP1' + frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account) + + pr = make_purchase_receipt( + company=company, item=service_item, + warehouse='Finished Goods - TCP1', do_not_save=1 + ) + item_row_with_diff_rate = frappe.copy_doc(pr.items[0]) + item_row_with_diff_rate.rate = 100 + pr.append('items', item_row_with_diff_rate) + + pr.save() + pr.submit() + + item_one_gl_entry = frappe.db.get_all("GL Entry", { + 'voucher_type': pr.doctype, + 'voucher_no': pr.name, + 'account': srbnb_account, + 'voucher_detail_no': pr.items[0].name + }, pluck="name") + + item_two_gl_entry = frappe.db.get_all("GL Entry", { + 'voucher_type': pr.doctype, + 'voucher_no': pr.name, + 'account': srbnb_account, + 'voucher_detail_no': pr.items[1].name + }, pluck="name") + + # check if the entries are not merged into one + # seperate entries should be made since voucher_detail_no is different + self.assertEqual(len(item_one_gl_entry), 1) + self.assertEqual(len(item_two_gl_entry), 1) + + frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From b081d7933262933dd8f31668d4d2ff1d15df5d12 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 14:52:00 +0530 Subject: [PATCH 144/376] fix(Asset Repair): Fix tests --- .../doctype/asset_repair/asset_repair.json | 2 +- .../doctype/asset_repair/asset_repair.py | 58 +++++++++---------- .../doctype/asset_repair/test_asset_repair.py | 7 ++- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 6f9b6863f7e..c25216c21bd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -264,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 15:20:24.056706", + "modified": "2021-06-20 17:35:51.075537", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 212af7a930b..5fccfb76a52 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,74 +5,73 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, cint -from frappe.model.document import Document +from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.controllers.accounts_controller import AccountsController class AssetRepair(AccountsController): def validate(self): - if self.repair_status == "Completed" and not self.completion_date: - self.completion_date = nowdate() - + self.asset_doc = frappe.get_doc('Asset', self.asset) self.update_status() - self.set_total_value() # change later + if self.get('stock_items'): + self.set_total_value() # change later self.calculate_total_repair_cost() def update_status(self): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: - asset = frappe.get_doc('Asset', self.asset) - asset.set_status() + self.asset_doc.set_status() def set_total_value(self): - for item in self.stock_items: + for item in self.get('stock_items'): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost - for item in self.stock_items: - self.total_repair_cost += item.total_value + if self.get('stock_items'): + for item in self.get('stock_items'): + self.total_repair_cost += item.total_value def on_submit(self): self.check_repair_status() - if self.stock_consumption or self.capitalize_repair_cost: + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): self.increase_asset_value() - if self.stock_consumption: + if self.get('stock_consumption'): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() - if self.capitalize_repair_cost: + if self.get('capitalize_repair_cost'): self.make_gl_entries() if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: self.modify_depreciation_schedule() + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.save() + def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) def check_for_stock_items_and_warehouse(self): - if not self.stock_items: + if not self.get('stock_items'): frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def increase_asset_value(self): total_value_of_stock_consumed = 0 - for item in self.stock_items: - total_value_of_stock_consumed += item.total_value + if self.get('stock_consumption'): + for item in self.get('stock_items'): + total_value_of_stock_consumed += item.total_value - asset = frappe.get_doc('Asset', self.asset) - asset.flags.ignore_validate_update_after_submit = True - if asset.calculate_depreciation: - for row in asset.finance_books: + if self.asset_doc.calculate_depreciation: + for row in self.asset_doc.finance_books: row.value_after_depreciation += total_value_of_stock_consumed if self.capitalize_repair_cost: row.value_after_depreciation += self.repair_cost - asset.save() def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ @@ -81,7 +80,7 @@ class AssetRepair(AccountsController): "company": self.company }) - for stock_item in self.stock_items: + for stock_item in self.get('stock_items'): stock_entry.append('items', { "s_warehouse": self.warehouse, "item_code": stock_item.item, @@ -121,7 +120,7 @@ class AssetRepair(AccountsController): }, item=self) ) - if self.stock_consumption: + if self.get('stock_consumption'): # creating GL Entries for each row in Stock Items based on the Stock Entry created for it stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) for item in stock_entry.items: @@ -158,18 +157,13 @@ class AssetRepair(AccountsController): return gl_entries def modify_depreciation_schedule(self): - asset = frappe.get_doc('Asset', self.asset) - asset.flags.ignore_validate_update_after_submit = True - for row in asset.finance_books: + for row in self.asset_doc.finance_books: row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation - asset.flags.increase_in_asset_life = False + self.asset_doc.flags.increase_in_asset_life = False extra_months = self.increase_in_asset_life % row.frequency_of_depreciation if extra_months != 0: - self.calculate_last_schedule_date(asset, row, extra_months) - - asset.prepare_depreciation_data() - asset.save() + self.calculate_last_schedule_date(self.asset_doc, row, extra_months) # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation def calculate_last_schedule_date(self, asset, row, extra_months): diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 9c9dd449713..d1b417fd384 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -105,7 +105,9 @@ class TestAssetRepair(unittest.TestCase): initial_num_of_depreciations = num_of_depreciations(asset) create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) asset.reload() + self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) + self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation) def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations @@ -126,7 +128,8 @@ def create_asset_repair(**args): "asset_name": asset.asset_name, "failure_date": nowdate(), "description": "Test Description", - "repair_cost": 0 + "repair_cost": 0, + "company": asset.company }) if args.stock_consumption: @@ -142,7 +145,7 @@ def create_asset_repair(**args): asset_repair.save() except frappe.DuplicateEntryError: pass - + if args.submit: asset_repair.repair_status = "Completed" asset_repair.cost_center = "_Test Cost Center - _TC" From 773aabae440a8d85d772ea0cd8f9749faee02dfc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jun 2021 14:42:40 +0530 Subject: [PATCH 145/376] fix: allow to select group warehouse while downloading materials from production plan --- .../production_plan/production_plan.js | 21 ++++++- .../production_plan/production_plan.py | 59 ++++++++++--------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 64d584118f2..056f600c3ba 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -306,8 +306,25 @@ frappe.ui.form.on('Production Plan', { }, download_materials_required: function(frm) { - let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); + const fields = [{ + fieldname: 'warehouses', + fieldtype: 'Table MultiSelect', + label: __('Warehouses'), + default: frm.doc.from_warehouse, + options: "Production Plan Material Request Warehouse", + get_query: function () { + return { + filters: { + company: frm.doc.company + } + }; + }, + }]; + + frappe.prompt(fields, (row) => { + let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses:row.warehouses }); + }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); }, show_progress: function(frm) { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 46e047654b5..0ede1bd4ab3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -98,7 +98,7 @@ class ProductionPlan(Document): def get_items(self): self.set('po_items', []) if self.get_items_from == "Sales Order": - self.get_so_items() + self.get_so_items() elif self.get_items_from == "Material Request": self.get_mr_items() @@ -170,11 +170,11 @@ class ProductionPlan(Document): refs = {} for data in items: item_details = get_item_details(data.item_code) - if self.combine_items: + if self.combine_items: if item_details.bom_no in refs: refs[item_details.bom_no]['so_details'].append({ 'sales_order': data.parent, - 'sales_order_item': data.name, + 'sales_order_item': data.name, 'qty': data.pending_qty }) refs[item_details.bom_no]['qty'] += data.pending_qty @@ -188,10 +188,10 @@ class ProductionPlan(Document): } refs[item_details.bom_no]['so_details'].append({ 'sales_order': data.parent, - 'sales_order_item': data.name, + 'sales_order_item': data.name, 'qty': data.pending_qty }) - + pi = self.append('po_items', { 'include_exploded_items': 1, 'warehouse': data.warehouse, @@ -209,12 +209,12 @@ class ProductionPlan(Document): pi.sales_order = data.parent pi.sales_order_item = data.name pi.description = data.description - + elif self.get_items_from == "Material Request": pi.material_request = data.parent pi.material_request_item = data.name pi.description = data.description - + if refs: for po_item in self.po_items: po_item.planned_qty = refs[po_item.bom_no]['qty'] @@ -477,18 +477,19 @@ class ProductionPlan(Document): msgprint(_("No material request created")) @frappe.whitelist() -def download_raw_materials(doc): +def download_raw_materials(doc, warehouses=None): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', - 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', - 'Safety Stock', 'Required Qty']] + 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', + 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] - for d in get_items_for_material_requests(doc): + doc.warehouse = None + for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True): item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), - d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) + d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) if not doc.get('for_warehouse'): row = {'item_code': d.get('item_code')} @@ -507,7 +508,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, bei.description, bei.stock_uom, 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.purchase_uom, item_uom.conversion_factor, item.safety_stock from `tabBOM Explosion Item` bei JOIN `tabBOM` bom ON bom.name = bei.parent @@ -677,32 +678,36 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, - ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` - where item_code = %(item_code)s {conditions} + ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, + ifnull(sum(planned_qty),0) as planned_qty + from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) +def get_warehouse_list(warehouses, warehouse_list=[]): + if isinstance(warehouses, string_types): + warehouses = json.loads(warehouses) + + for row in warehouses: + child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) + if child_warehouses: + warehouse_list.extend(child_warehouses) + else: + warehouse_list.append(row.get("warehouse")) + @frappe.whitelist() -def get_items_for_material_requests(doc, warehouses=None): +def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) warehouse_list = [] if warehouses: - if isinstance(warehouses, string_types): - warehouses = json.loads(warehouses) - - for row in warehouses: - child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) - if child_warehouses: - warehouse_list.extend(child_warehouses) - else: - warehouse_list.append(row.get("warehouse")) + get_warehouse_list(warehouses, warehouse_list) if warehouse_list: warehouses = list(set(warehouse_list)) - if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses: + if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: warehouses.remove(doc.get("for_warehouse")) warehouse_list = None @@ -795,7 +800,7 @@ def get_items_for_material_requests(doc, warehouses=None): if items: mr_items.append(items) - if not ignore_existing_ordered_qty and warehouses: + if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses: new_mr_items = [] for item in mr_items: get_materials_from_other_locations(item, warehouses, new_mr_items, company) From a34bf5edeceff190bf7c264cfa163443ea0aa71e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 14:55:34 +0530 Subject: [PATCH 146/376] fix: Rename 'Stock Item' to 'Asset Repair Consumed Item' --- .../assets/doctype/asset_maintenance/asset_maintenance.json | 4 ++-- erpnext/assets/doctype/asset_repair/asset_repair.json | 4 ++-- .../{stock_item => asset_repair_consumed_item}/__init__.py | 0 .../asset_repair_consumed_item.json} | 2 +- .../asset_repair_consumed_item.py} | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename erpnext/assets/doctype/{stock_item => asset_repair_consumed_item}/__init__.py (100%) rename erpnext/assets/doctype/{stock_item/stock_item.json => asset_repair_consumed_item/asset_repair_consumed_item.json} (96%) rename erpnext/assets/doctype/{stock_item/stock_item.py => asset_repair_consumed_item/asset_repair_consumed_item.py} (81%) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index da2fd754512..63a55389d81 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -126,11 +126,11 @@ "fieldname": "stock_items", "fieldtype": "Table", "label": "Stock Items", - "options": "Stock Item" + "options": "Asset Repair Consumed Item" } ], "links": [], - "modified": "2021-05-13 05:24:58.480132", + "modified": "2021-06-21 14:53:46.041123", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index c25216c21bd..6a14384f3ff 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -169,7 +169,7 @@ "fieldtype": "Table", "label": "Stock Items", "mandatory_depends_on": "stock_consumption", - "options": "Stock Item" + "options": "Asset Repair Consumed Item" }, { "fieldname": "section_break_23", @@ -264,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-20 17:35:51.075537", + "modified": "2021-06-21 14:53:46.665576", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/stock_item/__init__.py b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py similarity index 100% rename from erpnext/assets/doctype/stock_item/__init__.py rename to erpnext/assets/doctype/asset_repair_consumed_item/__init__.py diff --git a/erpnext/assets/doctype/stock_item/stock_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json similarity index 96% rename from erpnext/assets/doctype/stock_item/stock_item.json rename to erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index b1f05db3950..528f0ec986a 100644 --- a/erpnext/assets/doctype/stock_item/stock_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -46,7 +46,7 @@ "modified": "2021-05-12 03:19:55.006300", "modified_by": "Administrator", "module": "Assets", - "name": "Stock Item", + "name": "Asset Repair Consumed Item", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/assets/doctype/stock_item/stock_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py similarity index 81% rename from erpnext/assets/doctype/stock_item/stock_item.py rename to erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py index 0e3cc3f8ba7..fa22a5712f4 100644 --- a/erpnext/assets/doctype/stock_item/stock_item.py +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py @@ -4,5 +4,5 @@ # import frappe from frappe.model.document import Document -class StockItem(Document): +class AssetRepairConsumedItem(Document): pass From 01faa9ca116153fd8774fd245657f920f5fc8d28 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jun 2021 00:59:02 +0530 Subject: [PATCH 147/376] fix: rate not able to change in purchase order --- erpnext/controllers/taxes_and_totals.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2bb83ea7f0f..56da5b71da0 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object): item.margin_type = None item.margin_rate_or_amount = 0.0 - if item.margin_type and item.margin_rate_or_amount: + if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate): + item.margin_type = "Amount" + item.margin_rate_or_amount = flt(item.rate - item.price_list_rate, + item.precision("margin_rate_or_amount")) + item.rate_with_margin = item.rate + + elif item.margin_type and item.margin_rate_or_amount: margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 rate_with_margin = flt(item.price_list_rate) + flt(margin_value) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) From f88a13b292334475c6cc889eefafc2ccd556e987 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 15:02:40 +0530 Subject: [PATCH 148/376] fix(Asset Repair): Compute total_value instantly --- erpnext/assets/doctype/asset_repair/asset_repair.js | 7 +++++++ erpnext/assets/doctype/asset_repair/asset_repair.py | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index ced3dad1e5f..91bed4fdd03 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -35,6 +35,13 @@ frappe.ui.form.on('Asset Repair', { } }); +frappe.ui.form.on('Asset Repair Consumed Item', { + consumed_quantity: function(frm, cdt, cdn) { + var row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); + }, +}); + cur_frm.fields_dict.cost_center.get_query = function(doc) { return { filters: { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 5fccfb76a52..79b9a6a2b57 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -14,8 +14,6 @@ class AssetRepair(AccountsController): def validate(self): self.asset_doc = frappe.get_doc('Asset', self.asset) self.update_status() - if self.get('stock_items'): - self.set_total_value() # change later self.calculate_total_repair_cost() def update_status(self): @@ -24,10 +22,6 @@ class AssetRepair(AccountsController): else: self.asset_doc.set_status() - def set_total_value(self): - for item in self.get('stock_items'): - item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) - def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost if self.get('stock_items'): From 91a99e0d8973b97743065aa61f62635b13e05256 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 15:22:02 +0530 Subject: [PATCH 149/376] fix(Asset Repair): Increase stock quantity and decrease asset value on cancellation --- .../doctype/asset_repair/asset_repair.py | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 79b9a6a2b57..e7b8b45b7ee 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -24,9 +24,9 @@ class AssetRepair(AccountsController): def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost - if self.get('stock_items'): - for item in self.get('stock_items'): - self.total_repair_cost += item.total_value + + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + self.total_repair_cost += total_value_of_stock_consumed def on_submit(self): self.check_repair_status() @@ -44,6 +44,14 @@ class AssetRepair(AccountsController): self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.save() + def on_cancel(self): + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): + self.decrease_asset_value() + if self.get('stock_consumption'): + self.increase_stock_quantity() + if self.get('capitalize_repair_cost'): + self.make_gl_entries(cancel=True) + def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) @@ -55,10 +63,7 @@ class AssetRepair(AccountsController): frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def increase_asset_value(self): - total_value_of_stock_consumed = 0 - if self.get('stock_consumption'): - for item in self.get('stock_items'): - total_value_of_stock_consumed += item.total_value + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() if self.asset_doc.calculate_depreciation: for row in self.asset_doc.finance_books: @@ -67,6 +72,24 @@ class AssetRepair(AccountsController): if self.capitalize_repair_cost: row.value_after_depreciation += self.repair_cost + def decrease_asset_value(self): + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + + if self.asset_doc.calculate_depreciation: + for row in self.asset_doc.finance_books: + row.value_after_depreciation -= total_value_of_stock_consumed + + if self.capitalize_repair_cost: + row.value_after_depreciation -= self.repair_cost + + def get_total_value_of_stock_consumed(self): + total_value_of_stock_consumed = 0 + if self.get('stock_consumption'): + for item in self.get('stock_items'): + total_value_of_stock_consumed += item.total_value + + return total_value_of_stock_consumed + def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", @@ -86,8 +109,22 @@ class AssetRepair(AccountsController): self.stock_entry = stock_entry.name - def on_cancel(self): - self.make_gl_entries(cancel=True) + def increase_stock_quantity(self): + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Receipt", + "company": self.company + }) + + for stock_item in self.get('stock_items'): + stock_entry.append('items', { + "s_warehouse": self.warehouse, + "item_code": stock_item.item, + "qty": stock_item.consumed_quantity + }) + + stock_entry.insert() + stock_entry.submit() def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: From 8347eb1dbae8591cb42fd4a2398c7fad0cdcf6fb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 21 Jun 2021 15:38:44 +0530 Subject: [PATCH 150/376] Update production_plan.js --- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 056f600c3ba..450aa04a73d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -323,7 +323,7 @@ frappe.ui.form.on('Production Plan', { frappe.prompt(fields, (row) => { let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses:row.warehouses }); + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses }); }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); }, From 00c05108b218c66f8f1eb10e32dd110730a23c14 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jun 2021 14:42:40 +0530 Subject: [PATCH 151/376] fix: allow to select group warehouse while downloading materials from production plan --- .../production_plan/production_plan.js | 21 ++++++- .../production_plan/production_plan.py | 59 ++++++++++--------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 64d584118f2..056f600c3ba 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -306,8 +306,25 @@ frappe.ui.form.on('Production Plan', { }, download_materials_required: function(frm) { - let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); + const fields = [{ + fieldname: 'warehouses', + fieldtype: 'Table MultiSelect', + label: __('Warehouses'), + default: frm.doc.from_warehouse, + options: "Production Plan Material Request Warehouse", + get_query: function () { + return { + filters: { + company: frm.doc.company + } + }; + }, + }]; + + frappe.prompt(fields, (row) => { + let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses:row.warehouses }); + }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); }, show_progress: function(frm) { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 46e047654b5..0ede1bd4ab3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -98,7 +98,7 @@ class ProductionPlan(Document): def get_items(self): self.set('po_items', []) if self.get_items_from == "Sales Order": - self.get_so_items() + self.get_so_items() elif self.get_items_from == "Material Request": self.get_mr_items() @@ -170,11 +170,11 @@ class ProductionPlan(Document): refs = {} for data in items: item_details = get_item_details(data.item_code) - if self.combine_items: + if self.combine_items: if item_details.bom_no in refs: refs[item_details.bom_no]['so_details'].append({ 'sales_order': data.parent, - 'sales_order_item': data.name, + 'sales_order_item': data.name, 'qty': data.pending_qty }) refs[item_details.bom_no]['qty'] += data.pending_qty @@ -188,10 +188,10 @@ class ProductionPlan(Document): } refs[item_details.bom_no]['so_details'].append({ 'sales_order': data.parent, - 'sales_order_item': data.name, + 'sales_order_item': data.name, 'qty': data.pending_qty }) - + pi = self.append('po_items', { 'include_exploded_items': 1, 'warehouse': data.warehouse, @@ -209,12 +209,12 @@ class ProductionPlan(Document): pi.sales_order = data.parent pi.sales_order_item = data.name pi.description = data.description - + elif self.get_items_from == "Material Request": pi.material_request = data.parent pi.material_request_item = data.name pi.description = data.description - + if refs: for po_item in self.po_items: po_item.planned_qty = refs[po_item.bom_no]['qty'] @@ -477,18 +477,19 @@ class ProductionPlan(Document): msgprint(_("No material request created")) @frappe.whitelist() -def download_raw_materials(doc): +def download_raw_materials(doc, warehouses=None): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', - 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', - 'Safety Stock', 'Required Qty']] + 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', + 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] - for d in get_items_for_material_requests(doc): + doc.warehouse = None + for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True): item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), - d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) + d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) if not doc.get('for_warehouse'): row = {'item_code': d.get('item_code')} @@ -507,7 +508,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, bei.description, bei.stock_uom, 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.purchase_uom, item_uom.conversion_factor, item.safety_stock from `tabBOM Explosion Item` bei JOIN `tabBOM` bom ON bom.name = bei.parent @@ -677,32 +678,36 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, - ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` - where item_code = %(item_code)s {conditions} + ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, + ifnull(sum(planned_qty),0) as planned_qty + from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) +def get_warehouse_list(warehouses, warehouse_list=[]): + if isinstance(warehouses, string_types): + warehouses = json.loads(warehouses) + + for row in warehouses: + child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) + if child_warehouses: + warehouse_list.extend(child_warehouses) + else: + warehouse_list.append(row.get("warehouse")) + @frappe.whitelist() -def get_items_for_material_requests(doc, warehouses=None): +def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) warehouse_list = [] if warehouses: - if isinstance(warehouses, string_types): - warehouses = json.loads(warehouses) - - for row in warehouses: - child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) - if child_warehouses: - warehouse_list.extend(child_warehouses) - else: - warehouse_list.append(row.get("warehouse")) + get_warehouse_list(warehouses, warehouse_list) if warehouse_list: warehouses = list(set(warehouse_list)) - if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses: + if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: warehouses.remove(doc.get("for_warehouse")) warehouse_list = None @@ -795,7 +800,7 @@ def get_items_for_material_requests(doc, warehouses=None): if items: mr_items.append(items) - if not ignore_existing_ordered_qty and warehouses: + if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses: new_mr_items = [] for item in mr_items: get_materials_from_other_locations(item, warehouses, new_mr_items, company) From 6a07869e559159bc9ba3459d9ca2e66ec15af4e3 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 21 Jun 2021 15:41:30 +0530 Subject: [PATCH 152/376] Update production_plan.js --- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 056f600c3ba..450aa04a73d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -323,7 +323,7 @@ frappe.ui.form.on('Production Plan', { frappe.prompt(fields, (row) => { let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses:row.warehouses }); + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses }); }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); }, From 5776a962b3d12f25f39c88208e8e10c9897830fd Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 18:57:11 +0530 Subject: [PATCH 153/376] fix(Asset Repair): Return Depreciation Schedule to original state on cancellation --- .../doctype/asset_repair/asset_repair.py | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index e7b8b45b7ee..01b36880be0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -42,15 +42,25 @@ class AssetRepair(AccountsController): self.modify_depreciation_schedule() self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() self.asset_doc.save() def on_cancel(self): + self.asset_doc = frappe.get_doc('Asset', self.asset) + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): self.decrease_asset_value() if self.get('stock_consumption'): self.increase_stock_quantity() if self.get('capitalize_repair_cost'): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.make_gl_entries(cancel=True) + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: + self.revert_depreciation_schedule_on_cancellation() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() def check_repair_status(self): if self.repair_status == "Pending": @@ -101,7 +111,8 @@ class AssetRepair(AccountsController): stock_entry.append('items', { "s_warehouse": self.warehouse, "item_code": stock_item.item, - "qty": stock_item.consumed_quantity + "qty": stock_item.consumed_quantity, + "basic_rate": stock_item.valuation_rate }) stock_entry.insert() @@ -118,7 +129,7 @@ class AssetRepair(AccountsController): for stock_item in self.get('stock_items'): stock_entry.append('items', { - "s_warehouse": self.warehouse, + "t_warehouse": self.warehouse, "item_code": stock_item.item, "qty": stock_item.consumed_quantity }) @@ -126,6 +137,8 @@ class AssetRepair(AccountsController): stock_entry.insert() stock_entry.submit() + self.stock_entry = stock_entry.name + def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: gl_entries = self.get_gl_entries() @@ -216,6 +229,34 @@ class AssetRepair(AccountsController): if asset.to_date > schedule_date: row.total_number_of_depreciations += 1 + def revert_depreciation_schedule_on_cancellation(self): + for row in self.asset_doc.finance_books: + row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation + + self.asset_doc.flags.increase_in_asset_life = False + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months) + + def calculate_last_schedule_date_before_modification(self, asset, row, extra_months): + asset.flags.increase_in_asset_life = True + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ + cint(asset.number_of_depreciations_booked) + + # the Schedule Date in the final row of the modified Depreciation Schedule + last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + + # the Schedule Date in the final row of the original Depreciation Schedule + asset.to_date = add_months(last_schedule_date, -extra_months) + + # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations + # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... + schedule_date = add_months(row.depreciation_start_date, + (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation)) + + if asset.to_date < schedule_date: + row.total_number_of_depreciations -= 1 + @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) From f97206b3cbb9aeee730d67db8161f75138404595 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 19:38:37 +0530 Subject: [PATCH 154/376] fix: Sort website products by weightage mentioned in Item master --- erpnext/shopping_cart/product_query.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 36d446ed0fd..dd94c26bc60 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -61,7 +61,8 @@ class ProductQuery: ], or_filters=self.or_filters, start=start, - limit=self.page_length + limit=self.page_length, + order_by="weightage desc" ) items_dict = {item.name: item for item in items} @@ -71,7 +72,15 @@ class ProductQuery: result = [items_dict.get(item) for item in list(set.intersection(*all_items))] else: - result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length) + result = frappe.get_all( + "Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + start=start, + limit=self.page_length, + order_by="weightage desc" + ) for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') From c212d7fafda778a53d464f4cd798ae92a580c7da Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 19:38:37 +0530 Subject: [PATCH 155/376] fix: Sort website products by weightage mentioned in Item master --- erpnext/shopping_cart/product_query.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 36d446ed0fd..dd94c26bc60 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -61,7 +61,8 @@ class ProductQuery: ], or_filters=self.or_filters, start=start, - limit=self.page_length + limit=self.page_length, + order_by="weightage desc" ) items_dict = {item.name: item for item in items} @@ -71,7 +72,15 @@ class ProductQuery: result = [items_dict.get(item) for item in list(set.intersection(*all_items))] else: - result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length) + result = frappe.get_all( + "Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + start=start, + limit=self.page_length, + order_by="weightage desc" + ) for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') From cfdda21dd2a353cafbe7d2d349ad06714d6061dd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 13:03:22 +0530 Subject: [PATCH 156/376] fix: Export invoices not visible in GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 80e2d725a2d..10961593e1c 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -201,7 +201,7 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ - conditions += " AND billing_address_gstin NOT IN %(company_gstins)s" + conditions += " AND IFNULL(billing_address_gstin, '') NOT IN %(company_gstins)s" return conditions From 943b46a759b79fd4fae9a7ada0425d7947e3f746 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:26:36 +0530 Subject: [PATCH 157/376] fix: sql syntax error in get_project_name method (#26145) --- erpnext/controllers/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 7bd739a6ad3..4ada8344255 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") - and {cond} {match_cond} {scond} + and {cond} {scond} {match_cond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), idx desc, From 3b0eac79bf6b302d2b4800c2f6333828f00d4bff Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 16:26:09 +0530 Subject: [PATCH 158/376] fix(Asset Repair): Compute total_value --- erpnext/assets/doctype/asset_repair/asset_repair.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 01b36880be0..342a8861e60 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -14,6 +14,9 @@ class AssetRepair(AccountsController): def validate(self): self.asset_doc = frappe.get_doc('Asset', self.asset) self.update_status() + + if self.get('stock_items'): + self.set_total_value() self.calculate_total_repair_cost() def update_status(self): @@ -22,6 +25,10 @@ class AssetRepair(AccountsController): else: self.asset_doc.set_status() + def set_total_value(self): + for item in self.get('stock_items'): + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost From 889140fd8c5c8684f394a36516878a2490efe21c Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:26:19 +0530 Subject: [PATCH 159/376] fix: sql syntax error in get_project_name method (#26147) --- erpnext/controllers/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 7bd739a6ad3..4ada8344255 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") - and {cond} {match_cond} {scond} + and {cond} {scond} {match_cond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), idx desc, From e3a697c5820cc08a50172570d40561681b8262d0 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:27:53 +0530 Subject: [PATCH 160/376] fix: disable sales order cancellation if linked to draft invoice (#26104) --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- .../selling/doctype/sales_order/test_sales_order.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 551f715bd52..41f57a34d3d 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -233,7 +233,7 @@ class SalesOrder(SellingController): # Checks Sales Invoice submit_rv = frappe.db.sql_list("""select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 - where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", + where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""", self.name) if submit_rv: diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 987371066a7..974648d6d44 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1217,6 +1217,19 @@ class TestSalesOrder(unittest.TestCase): # To test if the SO does NOT have a Blanket Order self.assertEqual(so_doc.items[0].blanket_order, None) + def test_so_cancellation_when_si_drafted(self): + """ + Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state + Expected result: sales order should not get cancelled + """ + so = make_sales_order() + so.submit() + si = make_sales_invoice(so.name) + si.save() + + self.assertRaises(frappe.ValidationError, so.cancel) + + def make_sales_order(**args): so = frappe.new_doc("Sales Order") From 3a44d888661b0d3b81e475307484dc09ba4b2c78 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 16:28:29 +0530 Subject: [PATCH 161/376] fix(Asset): Fix tests for Asset Repair --- erpnext/assets/doctype/asset/test_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 29fbc9f15d6..f3667c7b95f 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -707,7 +707,7 @@ def create_asset(**args): "available_for_use_date": "2020-06-06", "location": "Test Location", "asset_owner": "Company", - "is_existing_asset": args.is_existing_asset or 0 + "is_existing_asset": 1 }) if asset.calculate_depreciation: From 219a87d53072cab01a79009f1195a359ce059a4a Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:28:58 +0530 Subject: [PATCH 162/376] fix: disable sales order cancellation if linked to draft invoice (#26125) --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- .../selling/doctype/sales_order/test_sales_order.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 551f715bd52..41f57a34d3d 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -233,7 +233,7 @@ class SalesOrder(SellingController): # Checks Sales Invoice submit_rv = frappe.db.sql_list("""select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 - where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", + where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""", self.name) if submit_rv: diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 987371066a7..974648d6d44 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1217,6 +1217,19 @@ class TestSalesOrder(unittest.TestCase): # To test if the SO does NOT have a Blanket Order self.assertEqual(so_doc.items[0].blanket_order, None) + def test_so_cancellation_when_si_drafted(self): + """ + Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state + Expected result: sales order should not get cancelled + """ + so = make_sales_order() + so.submit() + si = make_sales_invoice(so.name) + si.save() + + self.assertRaises(frappe.ValidationError, so.cancel) + + def make_sales_order(**args): so = frappe.new_doc("Sales Order") From 2ea325c0c3fbdc97e2aa1427f565ee176f543a4c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 16:33:10 +0530 Subject: [PATCH 163/376] fix(Asset Repair): Revert Stock Entry on cancellation --- .../doctype/asset_repair/asset_repair.py | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 342a8861e60..4261c535af2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -125,26 +125,11 @@ class AssetRepair(AccountsController): stock_entry.insert() stock_entry.submit() - self.stock_entry = stock_entry.name + self.db_set('stock_entry', stock_entry.name) def increase_stock_quantity(self): - stock_entry = frappe.get_doc({ - "doctype": "Stock Entry", - "stock_entry_type": "Material Receipt", - "company": self.company - }) - - for stock_item in self.get('stock_items'): - stock_entry.append('items', { - "t_warehouse": self.warehouse, - "item_code": stock_item.item, - "qty": stock_item.consumed_quantity - }) - - stock_entry.insert() - stock_entry.submit() - - self.stock_entry = stock_entry.name + stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + stock_entry.cancel() def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: From a446d61e10ed2083151a3aacceaa6e1315661f33 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 22 Jun 2021 16:51:15 +0530 Subject: [PATCH 164/376] fix: Changed profitability analysis report width --- .../profitability_analysis/profitability_analysis.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 60e675f2f1f..48bd7308bca 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -168,21 +168,24 @@ def get_columns(filters): "label": _("Income"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 305 + }, { "fieldname": "expense", "label": _("Expense"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 305 + }, { "fieldname": "gross_profit_loss", "label": _("Gross Profit / Loss"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 307 + } ] From 859c8c92f7a73f4b85fd7bea244169e69fda3b24 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 17:16:12 +0530 Subject: [PATCH 165/376] fix: Remove changes made to Asset Maintenance --- .../asset_maintenance/asset_maintenance.js | 3 -- .../asset_maintenance/asset_maintenance.json | 31 +--------------- .../asset_maintenance/asset_maintenance.py | 37 +------------------ 3 files changed, 3 insertions(+), 68 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 19393b7e9db..70b8654509f 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -30,10 +30,7 @@ frappe.ui.form.on('Asset Maintenance', { if(!frm.is_new()) { frm.trigger('make_dashboard'); } - - frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption); }, - make_dashboard: (frm) => { if(!frm.is_new()) { frappe.call({ diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index 63a55389d81..c0c2566fe23 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -12,17 +12,13 @@ "column_break_3", "item_code", "item_name", - "stock_consumption", "section_break_6", "maintenance_team", "column_break_9", "maintenance_manager", "maintenance_manager_name", "section_break_8", - "asset_maintenance_tasks", - "stock_consumption_details_section", - "warehouse", - "stock_items" + "asset_maintenance_tasks" ], "fields": [ { @@ -104,33 +100,10 @@ "label": "Maintenance Tasks", "options": "Asset Maintenance Task", "reqd": 1 - }, - { - "default": "0", - "fieldname": "stock_consumption", - "fieldtype": "Check", - "label": "Stock Consumed During Maintenance" - }, - { - "fieldname": "stock_consumption_details_section", - "fieldtype": "Section Break", - "label": "Stock Consumption Details" - }, - { - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "options": "Warehouse" - }, - { - "fieldname": "stock_items", - "fieldtype": "Table", - "label": "Stock Items", - "options": "Asset Repair Consumed Item" } ], "links": [], - "modified": "2021-06-21 14:53:46.041123", + "modified": "2020-05-28 20:28:32.993823", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index e3e654c398b..a506deec93e 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -19,45 +19,10 @@ class AssetMaintenance(Document): if not task.assign_to and self.docstatus == 0: throw(_("Row #{}: Please asign task to a member.").format(task.idx)) - if self.stock_consumption: - self.check_for_stock_items_and_warehouse() - self.increase_asset_value() - self.decrease_stock_quantity() - def on_update(self): for task in self.get('asset_maintenance_tasks'): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) - self.sync_maintenance_tasks() - - def check_for_stock_items_and_warehouse(self): - if self.stock_consumption: - if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Maintenance.")) - if not self.warehouse: - frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) - - def increase_asset_value(self): - asset_value = frappe.db.get_value('Asset', self.asset_name, 'asset_value') - for item in self.stock_items: - asset_value += item.total_value - - frappe.db.set_value('Asset', self.asset_name, 'asset_value', asset_value) - - def decrease_stock_quantity(self): - stock_entry = frappe.get_doc({ - "doctype": "Stock Entry", - "stock_entry_type": "Material Issue" - }) - - for stock_item in self.stock_items: - stock_entry.append('items', { - "s_warehouse": self.warehouse, - "item_code": stock_item.item, - "qty": stock_item.consumed_quantity - }) - - stock_entry.insert() - stock_entry.submit() + self.sync_maintenance_tasks() def sync_maintenance_tasks(self): tasks_names = [] From 2e4596540516a2f053f47e0902060b7e235b5d42 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 17:24:14 +0530 Subject: [PATCH 166/376] fix(Asset Repair): Fix Sider issues --- erpnext/assets/doctype/asset_repair/asset_repair.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 4261c535af2..6054258ea69 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -16,7 +16,7 @@ class AssetRepair(AccountsController): self.update_status() if self.get('stock_items'): - self.set_total_value() + self.set_total_value() self.calculate_total_repair_cost() def update_status(self): @@ -26,8 +26,8 @@ class AssetRepair(AccountsController): self.asset_doc.set_status() def set_total_value(self): - for item in self.get('stock_items'): - item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + for item in self.get('stock_items'): + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost From c05496a5a7c52e7f7c4a8a4f5c55f71b52d810a8 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 22 Jun 2021 17:53:53 +0530 Subject: [PATCH 167/376] fix: fixed rounding off ordered percent to 100 in condition (#26152) --- erpnext/stock/doctype/material_request/material_request.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 92c8d213878..6e66f9869c0 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -101,7 +101,8 @@ frappe.ui.form.on('Material Request', { } if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { - if (flt(frm.doc.per_ordered, 2) < 100) { + let precision = frappe.defaults.get_default("float_precision"); + if (flt(frm.doc.per_ordered, precision) < 100) { let add_create_pick_list_button = () => { frm.add_custom_button(__('Pick List'), () => frm.events.create_pick_list(frm), __('Create')); From ce72d0c2c157ae532dcb5f1d22adaa8ba7afb7bb Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 22 Jun 2021 17:54:16 +0530 Subject: [PATCH 168/376] fix: fixed rounding off ordered percent to 100 in condition (#26148) --- erpnext/stock/doctype/material_request/material_request.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 6585e1c78cf..5f53be0869e 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -101,7 +101,8 @@ frappe.ui.form.on('Material Request', { } if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { - if (flt(frm.doc.per_ordered, 2) < 100) { + let precision = frappe.defaults.get_default("float_precision"); + if (flt(frm.doc.per_ordered, precision) < 100) { let add_create_pick_list_button = () => { frm.add_custom_button(__('Pick List'), () => frm.events.create_pick_list(frm), __('Create')); From 2af8c171c1f3cbd2db5310ec850ee8f87cd47a4f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 19:48:08 +0530 Subject: [PATCH 169/376] fix: Taxes on Internal Transfer payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b6b2bef9633..adaf99a7900 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -706,7 +706,7 @@ class PaymentEntry(AccountsController): if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if self.payment_type == 'Pay': + if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" @@ -761,7 +761,7 @@ class PaymentEntry(AccountsController): return self.advance_tax_account elif self.payment_type == 'Receive': return self.paid_from - elif self.payment_type == 'Pay': + elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to def update_advance_paid(self): From 7b0d86ead56252fc6ec2a4a52fabb9b7a50fecae Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 20:38:35 +0530 Subject: [PATCH 170/376] fix: Do not show received amount after tax for internal tarnsfers --- erpnext/accounts/doctype/payment_entry/payment_entry.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 54623dd6cd5..51f18a5a4e3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -690,7 +690,7 @@ "options": "Account" }, { - "depends_on": "eval:doc.received_amount", + "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", "label": "Received Amount After Tax", @@ -707,7 +707,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-09 11:55:04.215050", + "modified": "2021-06-22 20:37:06.154206", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 3b225b13e8c0ca2f5eee2dc56c3d1ce060a2842a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Jun 2021 21:18:20 +0530 Subject: [PATCH 171/376] refactor(pos): use pos invoice item name as unique identifier --- .../page/point_of_sale/pos_controller.js | 123 ++++++++++-------- .../page/point_of_sale/pos_item_cart.js | 29 +---- .../page/point_of_sale/pos_item_details.js | 86 +++++------- .../page/point_of_sale/pos_item_selector.js | 6 +- 4 files changed, 113 insertions(+), 131 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ae3f9e3c9d9..4c938756c76 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -241,8 +241,8 @@ erpnext.PointOfSale.Controller = class { events: { get_frm: () => this.frm, - cart_item_clicked: (item_code, batch_no, uom, rate) => { - const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); + cart_item_clicked: (item) => { + const item_row = this.get_item_from_frm(item); this.item_details.toggle_item_details_section(item_row); }, @@ -273,17 +273,16 @@ erpnext.PointOfSale.Controller = class { this.cart.toggle_numpad(minimize); }, - form_updated: (cdt, cdn, fieldname, value) => { - const item_row = frappe.model.get_doc(cdt, cdn); - if (item_row && item_row[fieldname] != value) { + form_updated: (item, field, value) => { + const item_row = frappe.model.get_doc(item.doctype, item.name); + if (item_row && item_row[field] != value) { - const { item_code, batch_no, uom, rate } = this.item_details.current_item; - const event = { - field: fieldname, + const args = { + field, value, - item: { item_code, batch_no, uom, rate } + item: this.item_details.current_item } - return this.on_cart_update(event) + return this.on_cart_update(args) } return Promise.resolve(); @@ -300,19 +299,18 @@ erpnext.PointOfSale.Controller = class { set_value_in_current_cart_item: (selector, value) => { this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item); }, - clone_new_batch_item_in_frm: (batch_serial_map, current_item) => { + clone_new_batch_item_in_frm: (batch_serial_map, item) => { // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches // for each unique batch new item row is added in the form & cart Object.keys(batch_serial_map).forEach(batch => { - const { item_code, batch_no } = current_item; - const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no); + const item_to_clone = this.frm.doc.items.find(i => i.name == item.name); const new_row = this.frm.add_child("items", { ...item_to_clone }); // update new serialno and batch new_row.batch_no = batch; new_row.serial_no = batch_serial_map[batch].join(`\n`); new_row.qty = batch_serial_map[batch].length; this.frm.doc.items.forEach(row => { - if (item_code === row.item_code) { + if (item.item_code === row.item_code) { this.update_cart_html(row); } }); @@ -321,8 +319,8 @@ erpnext.PointOfSale.Controller = class { remove_item_from_cart: () => this.remove_item_from_cart(), get_item_stock_map: () => this.item_stock_map, close_item_details: () => { - this.item_details.toggle_item_details_section(undefined); - this.cart.prev_action = undefined; + this.item_details.toggle_item_details_section(null); + this.cart.prev_action = null; this.cart.toggle_item_highlight(); }, get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse) @@ -506,50 +504,47 @@ erpnext.PointOfSale.Controller = class { let item_row = undefined; try { let { field, value, item } = args; - const { item_code, batch_no, serial_no, uom, rate } = item; - item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); + item_row = this.get_item_from_frm(item); + const item_row_exists = !$.isEmptyObject(item_row); - const item_selected_from_selector = field === 'qty' && value === "+1" + const from_selector = field === 'qty' && value === "+1"; + if (from_selector) + value = flt(item_row.qty) + flt(value); - if (item_row) { - item_selected_from_selector && (value = item_row.qty + flt(value)) - - field === 'qty' && (value = flt(value)); + if (item_row_exists) { + if (field === 'qty') + value = flt(value); if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); } - if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) { + if (this.is_current_item_being_edited(item_row) || from_selector) { await frappe.model.set_value(item_row.doctype, item_row.name, field, value); this.update_cart_html(item_row); } } else { - if (!this.frm.doc.customer) { - frappe.dom.unfreeze(); - frappe.show_alert({ - message: __('You must select a customer before adding an item.'), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); + if (!this.frm.doc.customer) + return this.raise_customer_selection_alert(); + + const { item_code, batch_no, serial_no, rate } = item; + + if (!item_code) return; - } - if (!item_code) return; - item_selected_from_selector && (value = flt(value)) - - const args = { item_code, batch_no, rate, [field]: value }; + const new_item = { item_code, batch_no, rate, [field]: value }; if (serial_no) { await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); - args['serial_no'] = serial_no; + new_item['serial_no'] = serial_no; } - if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0; + if (field === 'serial_no') + new_item['qty'] = value.split(`\n`).length || 0; - item_row = this.frm.add_child('items', args); + item_row = this.frm.add_child('items', new_item); if (field === 'qty' && value !== 0 && !this.allow_negative_stock) await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); @@ -558,8 +553,11 @@ erpnext.PointOfSale.Controller = class { this.update_cart_html(item_row); - this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row); - this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row); + if (this.item_details.$component.is(':visible')) + this.edit_item_details_of(item_row); + + if (this.check_serial_batch_selection_needed(item_row)) + this.edit_item_details_of(item_row); } } catch (error) { @@ -570,14 +568,33 @@ erpnext.PointOfSale.Controller = class { } } - get_item_from_frm(item_code, batch_no, uom, rate) { - const has_batch_no = batch_no; - return this.frm.doc.items.find( - i => i.item_code === item_code - && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) - && (i.uom === uom) - && (i.rate == rate) - ); + raise_customer_selection_alert() { + frappe.dom.unfreeze(); + frappe.show_alert({ + message: __('You must select a customer before adding an item.'), + indicator: 'orange' + }); + frappe.utils.play_sound("error"); + } + + get_item_from_frm({ name, item_code, batch_no, uom, rate }) { + let item_row = null; + if (name) { + item_row = this.frm.doc.items.find(i => i.name == name); + } else { + // if item is clicked twice from item selector + // then "item_code, batch_no, uom, rate" will help in getting the exact item + // to increase the qty by one + const has_batch_no = batch_no; + item_row = this.frm.doc.items.find( + i => i.item_code === item_code + && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) + && (i.uom === uom) + && (i.rate == rate) + ); + } + + return item_row || {}; } edit_item_details_of(item_row) { @@ -585,9 +602,7 @@ erpnext.PointOfSale.Controller = class { } is_current_item_being_edited(item_row) { - const { item_code, batch_no } = this.item_details.current_item; - - return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true; + return item_row.name == this.item_details.current_item.name; } update_cart_html(item_row, remove_item) { @@ -669,7 +684,7 @@ erpnext.PointOfSale.Controller = class { update_item_field(value, field_or_action) { if (field_or_action === 'checkout') { - this.item_details.toggle_item_details_section(undefined); + this.item_details.toggle_item_details_section(null); } else if (field_or_action === 'remove') { this.remove_item_from_cart(); } else { @@ -688,7 +703,7 @@ erpnext.PointOfSale.Controller = class { .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); - this.item_details.toggle_item_details_section(undefined); + this.item_details.toggle_item_details_section(null); frappe.dom.unfreeze(); }) .catch(e => console.log(e)); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index f5019f5083c..9de7beff46d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -181,11 +181,8 @@ erpnext.PointOfSale.ItemCart = class { me.$totals_section.find(".edit-cart-btn").click(); } - const item_code = unescape($cart_item.attr('data-item-code')); - const batch_no = unescape($cart_item.attr('data-batch-no')); - const uom = unescape($cart_item.attr('data-uom')); - const rate = unescape($cart_item.attr('data-rate')); - me.events.cart_item_clicked(item_code, batch_no, uom, rate); + const item_row_name = unescape($cart_item.attr('data-row-name')); + me.events.cart_item_clicked({ name: item_row_name }); this.numpad_value = ''; }); @@ -521,25 +518,14 @@ erpnext.PointOfSale.ItemCart = class { } } - get_cart_item({ item_code, batch_no, uom, rate }) { - const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; - const item_code_attr = `[data-item-code="${escape(item_code)}"]`; - const uom_attr = `[data-uom="${escape(uom)}"]`; - const rate_attr = `[data-rate="${escape(rate)}"]`; - - const item_selector = batch_no ? - `.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`; - + get_cart_item({ name }) { + const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; return this.$cart_items_wrapper.find(item_selector); } get_item_from_frm(item) { const doc = this.events.get_frm().doc; - const { item_code, batch_no, uom, rate } = item; - const search_field = batch_no ? 'batch_no' : 'item_code'; - const search_value = batch_no || item_code; - - return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate); + return doc.items.find(i => i.name == item.name); } update_item_html(item, remove_item) { @@ -564,10 +550,7 @@ erpnext.PointOfSale.ItemCart = class { if (!$item_to_update.length) { this.$cart_items_wrapper.append( - `
    -
    + `
    ` ) $item_to_update = this.get_cart_item(item_data); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 5e09df8efed..43a29b9c752 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -54,36 +54,28 @@ erpnext.PointOfSale.ItemDetails = class { this.$dicount_section = this.$component.find('.discount-section'); } - has_item_has_changed(item) { - const { item_code, batch_no, uom, rate } = this.current_item; - const item_code_is_same = item && item_code === item.item_code; - const batch_is_same = item && batch_no == item.batch_no; - const uom_is_same = item && uom === item.uom; - const rate_is_same = item && rate === item.rate; - - if (!item) - return false; - - if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same) - return false; - - return true; + compare_with_current_item(item) { + // returns true if `item` is currently being edited + return item && item.name == this.current_item.name } toggle_item_details_section(item) { - this.item_has_changed = this.has_item_has_changed(item); + const current_item_changed = !this.compare_with_current_item(item); - this.events.toggle_item_selector(this.item_has_changed); - this.toggle_component(this.item_has_changed); + // if item is null or highlighted cart item is clicked twice + const hide_item_details = !Boolean(item) || !current_item_changed; + + this.events.toggle_item_selector(!hide_item_details); + this.toggle_component(!hide_item_details); - if (this.item_has_changed) { + if (item && current_item_changed) { this.doctype = item.doctype; this.item_meta = frappe.get_meta(this.doctype); this.name = item.name; this.item_row = item; this.currency = this.events.get_frm().doc.currency; - this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate }; + this.current_item = item this.render_dom(item); this.render_discount_dom(item); @@ -180,7 +172,7 @@ erpnext.PointOfSale.ItemDetails = class { df: { ...field_meta, onchange: function() { - me.events.form_updated(me.doctype, me.name, fieldname, this.value); + me.events.form_updated(me.current_item, fieldname, this.value); } }, parent: this.$form_container.find(`.${fieldname}-control`), @@ -218,22 +210,17 @@ erpnext.PointOfSale.ItemDetails = class { bind_custom_control_change_event() { const me = this; if (this.rate_control) { - if (this.allow_rate_change) { - this.rate_control.df.onchange = function() { - if (this.value || flt(this.value) === 0) { - me.events.set_value_in_current_cart_item('rate', this.value); - me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { - const item_row = frappe.get_doc(me.doctype, me.name); - const doc = me.events.get_frm().doc; - me.$item_price.html(format_currency(item_row.rate, doc.currency)); - me.render_discount_dom(item_row); - }); - me.current_item.rate = this.value; - } - }; - } else { - this.rate_control.df.read_only = 1; - } + this.rate_control.df.onchange = function() { + if (this.value || flt(this.value) === 0) { + me.events.form_updated(me.current_item, 'rate', this.value).then(() => { + const item_row = frappe.get_doc(me.doctype, me.name); + const doc = me.events.get_frm().doc; + me.$item_price.html(format_currency(item_row.rate, doc.currency)); + me.render_discount_dom(item_row); + }); + } + }; + this.rate_control.df.read_only = !this.allow_rate_change; this.rate_control.refresh(); } @@ -246,7 +233,7 @@ erpnext.PointOfSale.ItemDetails = class { this.warehouse_control.df.reqd = 1; this.warehouse_control.df.onchange = function() { if (this.value) { - me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => { + me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { me.item_stock_map = me.events.get_item_stock_map(); const available_qty = me.item_stock_map[me.item_row.item_code][this.value]; if (available_qty === undefined) { @@ -278,7 +265,7 @@ erpnext.PointOfSale.ItemDetails = class { this.serial_no_control.df.reqd = 1; this.serial_no_control.df.onchange = async function() { !me.current_item.batch_no && await me.auto_update_batch_no(); - me.events.form_updated(me.doctype, me.name, 'serial_no', this.value); + me.events.form_updated(me.current_item, 'serial_no', this.value); } this.serial_no_control.refresh(); } @@ -295,19 +282,12 @@ erpnext.PointOfSale.ItemDetails = class { } } }; - this.batch_no_control.df.onchange = function() { - me.events.set_value_in_current_cart_item('batch-no', this.value); - me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); - me.current_item.batch_no = this.value; - } this.batch_no_control.refresh(); } if (this.uom_control) { this.uom_control.df.onchange = function() { - me.events.set_value_in_current_cart_item('uom', this.value); - me.events.form_updated(me.doctype, me.name, 'uom', this.value); - me.current_item.uom = this.value; + me.events.form_updated(me.current_item, 'uom', this.value); const item_row = frappe.get_doc(me.doctype, me.name); me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value); @@ -317,9 +297,9 @@ erpnext.PointOfSale.ItemDetails = class { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { const field_control = this[`${fieldname}_control`]; - const item_is_same = !this.has_item_has_changed(item_row); + const item_row_is_being_edited = this.compare_with_current_item(item_row); - if (item_is_same && field_control && field_control.get_value() !== value) { + if (item_row_is_being_edited && field_control && field_control.get_value() !== value) { field_control.set_value(value); cur_pos.update_cart_html(item_row); } @@ -337,7 +317,9 @@ erpnext.PointOfSale.ItemDetails = class { fields: ["batch_no", "name"] }); const batch_serial_map = serials_with_batch_no.reduce((acc, r) => { - acc[r.batch_no] || (acc[r.batch_no] = []); + if (!acc[r.batch_no]) { + acc[r.batch_no] = []; + } acc[r.batch_no] = [...acc[r.batch_no], r.name]; return acc; }, {}); @@ -353,12 +335,10 @@ erpnext.PointOfSale.ItemDetails = class { if (serial_nos_belongs_to_other_batch) { this.serial_no_control.set_value(batch_serial_nos); this.qty_control.set_value(batch_serial_map[batch_no].length); - } - delete batch_serial_map[batch_no]; - - if (serial_nos_belongs_to_other_batch) + delete batch_serial_map[batch_no]; this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); + } } } diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 64c529ee4a3..dd7f143c4cb 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -232,7 +232,11 @@ erpnext.PointOfSale.ItemSelector = class { uom = uom === "undefined" ? undefined : uom; rate = rate === "undefined" ? undefined : rate; - me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }}); + me.events.item_selected({ + field: 'qty', + value: "+1", + item: { item_code, batch_no, serial_no, uom, rate } + }); me.set_search_value(''); }); From 75ce0231703b4f621071350dc47edf0ac2600182 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Jun 2021 21:18:44 +0530 Subject: [PATCH 172/376] fix: hide images from cart & details --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 2 +- erpnext/selling/page/point_of_sale/pos_item_details.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 9de7beff46d..7cae0e47974 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class { function get_item_image_html() { const { image, item_name } = item_data; - if (image) { + if (!me.hide_images && image) { return `
    Date: Wed, 23 Jun 2021 11:02:05 +0530 Subject: [PATCH 173/376] fix: cannot cancel payment entry if linked with invoices (#26075) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d3ac3a66760..439b1edbce6 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice']; + if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); From fc98abece9b6c1975b228e8b37b8921d45a3bfac Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 23 Jun 2021 11:21:38 +0530 Subject: [PATCH 174/376] feat: Employee Grievance (#25705) * feat: Employee Grievance * feat: link to desk and automatic unsuspend * test: Employee Grievance * fix: Sider and Translation * fix: sider * fix: formatting * feat: changes requested * feat: Employee Grievance * feat: link to desk and automatic unsuspend * test: Employee Grievance * fix: Sider and Translation * fix: sider * fix: formatting * feat: changes requested * fix: patch test and sider issue * fix: make Employee Responsible non-mandatory since there cannot be an employee responsible for all sorts of grievances - show pay cut and suspension buttons only if Employee Resposible is set - some label changes * feat: added subject field for more context - set title for documents - added list view settings - refactor suspend and unsuspend functions - add submit and cancel perms for system and hr managers - fix tests * fix: sider issues * fix: removed suspension and paycut * fix:sider * fix: test * fix: test * fix: resolved Conflicts * fix: sider * fix: remove debugging print statements * fix: validation message * fix: unnecessary comma Co-authored-by: Rucha Mahabal --- erpnext/controllers/queries.py | 2 +- erpnext/hr/doctype/employee/employee.json | 4 +- erpnext/hr/doctype/employee/employee.py | 5 +- .../hr/doctype/employee/employee_dashboard.py | 5 +- erpnext/hr/doctype/employee/employee_list.js | 2 +- .../hr/doctype/employee_grievance/__init__.py | 0 .../employee_grievance/employee_grievance.js | 39 +++ .../employee_grievance.json | 261 ++++++++++++++++++ .../employee_grievance/employee_grievance.py | 15 + .../employee_grievance_list.js | 12 + .../test_employee_grievance.py | 51 ++++ erpnext/hr/doctype/grievance_type/__init__.py | 0 .../doctype/grievance_type/grievance_type.js | 8 + .../grievance_type/grievance_type.json | 70 +++++ .../doctype/grievance_type/grievance_type.py | 8 + .../grievance_type/test_grievance_type.py | 8 + erpnext/hr/workspace/hr/hr.json | 20 +- .../additional_salary/additional_salary.js | 17 +- .../doctype/salary_slip/test_salary_slip.py | 1 + .../salary_structure/test_salary_structure.py | 4 +- 20 files changed, 516 insertions(+), 16 deletions(-) create mode 100644 erpnext/hr/doctype/employee_grievance/__init__.py create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.js create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.json create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.py create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance_list.js create mode 100644 erpnext/hr/doctype/employee_grievance/test_employee_grievance.py create mode 100644 erpnext/hr/doctype/grievance_type/__init__.py create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.js create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.json create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.py create mode 100644 erpnext/hr/doctype/grievance_type/test_grievance_type.py diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 4ada8344255..280319321f2 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Employee", ["name", "employee_name"]) return frappe.db.sql("""select {fields} from `tabEmployee` - where status = 'Active' + where status in ('Active', 'Suspended') and docstatus < 2 and ({key} like %(txt)s or employee_name like %(txt)s) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 5442ed56c31..d592a9c79e2 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -207,7 +207,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Active\nInactive\nLeft", + "options": "Active\nInactive\nSuspended\nLeft", "reqd": 1, "search_index": 1 }, @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2021-06-12 11:31:37.730760", + "modified": "2021-06-17 11:31:37.730760", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index bc5694226ad..fa017d9d4c2 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr +from frappe.utils import getdate, validate_email_address, today, add_years, cstr from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ @@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet -from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass @@ -37,7 +36,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Active", "Inactive", "Left"]) + validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"]) self.employee = self.name self.set_employee_name() diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 285374d9f69..e853bee69f2 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -7,7 +7,8 @@ def get_data(): 'heatmap_message': _('This is based on the attendance of this Employee'), 'fieldname': 'employee', 'non_standard_fieldnames': { - 'Bank Account': 'party' + 'Bank Account': 'party', + 'Employee Grievance': 'raised_by' }, 'transactions': [ { @@ -20,7 +21,7 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] + 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] }, { 'label': _('Shift'), diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js index 6679e318c24..d37e1496ca3 100644 --- a/erpnext/hr/doctype/employee/employee_list.js +++ b/erpnext/hr/doctype/employee/employee_list.js @@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = { filters: [["status","=", "Active"]], get_indicator: function(doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; + indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status]; return indicator; } }; diff --git a/erpnext/hr/doctype/employee_grievance/__init__.py b/erpnext/hr/doctype/employee_grievance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.js b/erpnext/hr/doctype/employee_grievance/employee_grievance.js new file mode 100644 index 00000000000..25c5badbc77 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.js @@ -0,0 +1,39 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Grievance', { + setup: function(frm) { + frm.set_query('grievance_against_party', function() { + return { + filters: { + name: ['in', [ + 'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee'] + ] + } + }; + }); + frm.set_query('associated_document_type', function() { + let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website", + "Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"]; + return { + filters: { + istable: 0, + issingle: 0, + module: ["Not In", ignore_modules] + } + }; + }); + }, + + grievance_against_party: function(frm) { + let filters = {}; + if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) { + filters.name = ["!=", frm.doc.raised_by]; + } + frm.set_query('grievance_against', function() { + return { + filters: filters + }; + }); + }, +}); diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.json b/erpnext/hr/doctype/employee_grievance/employee_grievance.json new file mode 100644 index 00000000000..5a918562afb --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.json @@ -0,0 +1,261 @@ +{ + "actions": [], + "autoname": "HR-GRIEV-.YYYY.-.#####", + "creation": "2021-05-11 13:41:51.485295", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "raised_by", + "employee_name", + "designation", + "column_break_3", + "date", + "status", + "reports_to", + "grievance_details_section", + "grievance_against_party", + "grievance_against", + "grievance_type", + "column_break_11", + "associated_document_type", + "associated_document", + "section_break_14", + "description", + "investigation_details_section", + "cause_of_grievance", + "resolution_details_section", + "resolved_by", + "resolution_date", + "employee_responsible", + "column_break_16", + "resolution_detail", + "amended_from" + ], + "fields": [ + { + "fieldname": "grievance_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Type", + "options": "Grievance Type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date ", + "reqd": 1 + }, + { + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Open\nInvestigated\nResolved\nInvalid", + "reqd": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "cause_of_grievance", + "fieldtype": "Text", + "label": "Cause of Grievance", + "mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\"" + }, + { + "fieldname": "resolution_details_section", + "fieldtype": "Section Break", + "label": "Resolution Details" + }, + { + "fieldname": "resolved_by", + "fieldtype": "Link", + "label": "Resolved By", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"", + "options": "User" + }, + { + "fieldname": "employee_responsible", + "fieldtype": "Link", + "label": "Employee Responsible ", + "options": "Employee" + }, + { + "fieldname": "resolution_detail", + "fieldtype": "Small Text", + "label": "Resolution Details", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "resolution_date", + "fieldtype": "Date", + "label": "Resolution Date", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "grievance_against", + "fieldtype": "Dynamic Link", + "label": "Grievance Against", + "options": "grievance_against_party", + "reqd": 1 + }, + { + "fieldname": "raised_by", + "fieldtype": "Link", + "label": "Raised By", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Grievance", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "raised_by.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fetch_from": "raised_by.reports_to", + "fieldname": "reports_to", + "fieldtype": "Link", + "label": "Reports To", + "options": "Employee", + "read_only": 1 + }, + { + "fieldname": "grievance_details_section", + "fieldtype": "Section Break", + "label": "Grievance Details" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Section Break" + }, + { + "fieldname": "grievance_against_party", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Against Party", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "associated_document_type", + "fieldtype": "Link", + "label": "Associated Document Type", + "options": "DocType" + }, + { + "fieldname": "associated_document", + "fieldtype": "Dynamic Link", + "label": "Associated Document", + "options": "associated_document_type" + }, + { + "fieldname": "investigation_details_section", + "fieldtype": "Section Break", + "label": "Investigation Details" + }, + { + "fetch_from": "raised_by.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-06-21 12:51:01.499486", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Grievance", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "search_fields": "subject,raised_by,grievance_against_party", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "subject", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py new file mode 100644 index 00000000000..503b5ea4449 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, bold +from frappe.model.document import Document + +class EmployeeGrievance(Document): + def on_submit(self): + if self.status not in ["Invalid", "Resolved"]: + frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format( + bold("Invalid"), + bold("Resolved")) + ) + diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js new file mode 100644 index 00000000000..fc08e216099 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Employee Grievance"] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + var colors = { + "Open": "red", + "Investigated": "orange", + "Resolved": "green", + "Invalid": "grey" + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py new file mode 100644 index 00000000000..a615b20a5a2 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py @@ -0,0 +1,51 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +import unittest +from frappe.utils import today +from erpnext.hr.doctype.employee.test_employee import make_employee +class TestEmployeeGrievance(unittest.TestCase): + def test_create_employee_grievance(self): + create_employee_grievance() + +def create_employee_grievance(): + grievance_type = create_grievance_type() + emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company") + emp_2 = make_employee("testculprit@example.com", company="_Test Company") + + grievance = frappe.new_doc("Employee Grievance") + grievance.subject = "Test Employee Grievance" + grievance.raised_by = emp_1 + grievance.date = today() + grievance.grievance_type = grievance_type + grievance.grievance_against_party = "Employee" + grievance.grievance_against = emp_2 + grievance.description = "test descrip" + + #set cause + grievance.cause_of_grievance = "test cause" + + #resolution details + grievance.resolution_date = today() + grievance.resolution_detail = "test resolution detail" + grievance.resolved_by = "test_emp_grievance_@example.com" + grievance.employee_responsible = emp_2 + grievance.status = "Resolved" + + grievance.save() + grievance.submit() + + return grievance + + +def create_grievance_type(): + if frappe.db.exists("Grievance Type", "Employee Abuse"): + return frappe.get_doc("Grievance Type", "Employee Abuse") + grievance_type = frappe.new_doc("Grievance Type") + grievance_type.name = "Employee Abuse" + grievance_type.description = "Test" + grievance_type.save() + + return grievance_type.name + diff --git a/erpnext/hr/doctype/grievance_type/__init__.py b/erpnext/hr/doctype/grievance_type/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.js b/erpnext/hr/doctype/grievance_type/grievance_type.js new file mode 100644 index 00000000000..425f2fd5b5e --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Grievance Type', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.json b/erpnext/hr/doctype/grievance_type/grievance_type.json new file mode 100644 index 00000000000..1dce00a0e22 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-05-11 12:41:50.256071", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "description" + ], + "fields": [ + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-06-21 12:54:37.764712", + "modified_by": "Administrator", + "module": "HR", + "name": "Grievance Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.py b/erpnext/hr/doctype/grievance_type/grievance_type.py new file mode 100644 index 00000000000..618cf0a0318 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class GrievanceType(Document): + pass diff --git a/erpnext/hr/doctype/grievance_type/test_grievance_type.py b/erpnext/hr/doctype/grievance_type/test_grievance_type.py new file mode 100644 index 00000000000..a02a34d41fa --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/test_grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestGrievanceType(unittest.TestCase): + pass diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index c5201c22c9b..4500ba4560c 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -153,6 +153,24 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grievance Type", + "link_to": "Grievance Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grievance", + "link_to": "Employee Grievance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Employee", "hidden": 0, @@ -823,7 +841,7 @@ "type": "Link" } ], - "modified": "2021-04-26 13:36:15.413819", + "modified": "2021-05-13 17:19:40.524444", "modified_by": "Administrator", "module": "HR", "name": "HR", diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d1ed91fac70..24ffce537c1 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', { } }; }); + }, - frm.trigger('set_earning_component'); + onload: function(frm) { + if (frm.doc.type) { + frm.trigger('set_component_query'); + } }, employee: function(frm) { @@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', { }, company: function(frm) { - frm.trigger('set_earning_component'); + frm.set_value("type", ""); + frm.trigger('set_component_query'); }, - set_earning_component: function(frm) { + set_component_query: function(frm) { if (!frm.doc.company) return; + let filters = {company: frm.doc.company}; + if (frm.doc.type) { + filters.type = frm.doc.type; + } frm.set_query("salary_component", function() { return { - filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} + filters: filters }; }); }, diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 9e7db977ab1..ce88cc3f1e1 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", {"user_id": user}) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index dce6b7aa3d4..e7d123c9960 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, "doctype": "Salary Structure", "name": salary_structure, "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), + "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, "payment_account": get_random("Account", filters={'account_currency': currency}), "currency": currency From 11c155e6ce37eda56ed02e0614a060b8fc16c8fb Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:13:32 +0530 Subject: [PATCH 175/376] feat: Employee Grievance (#25705) (#26162) * feat: Employee Grievance * feat: link to desk and automatic unsuspend * test: Employee Grievance * fix: Sider and Translation * fix: sider * fix: formatting * feat: changes requested * feat: Employee Grievance * feat: link to desk and automatic unsuspend * test: Employee Grievance * fix: Sider and Translation * fix: sider * fix: formatting * feat: changes requested * fix: patch test and sider issue * fix: make Employee Responsible non-mandatory since there cannot be an employee responsible for all sorts of grievances - show pay cut and suspension buttons only if Employee Resposible is set - some label changes * feat: added subject field for more context - set title for documents - added list view settings - refactor suspend and unsuspend functions - add submit and cancel perms for system and hr managers - fix tests * fix: sider issues * fix: removed suspension and paycut * fix:sider * fix: test * fix: test * fix: resolved Conflicts * fix: sider * fix: remove debugging print statements * fix: validation message * fix: unnecessary comma Co-authored-by: Rucha Mahabal Co-authored-by: Rucha Mahabal --- erpnext/controllers/queries.py | 2 +- erpnext/hr/doctype/employee/employee.json | 4 +- erpnext/hr/doctype/employee/employee.py | 5 +- .../hr/doctype/employee/employee_dashboard.py | 5 +- erpnext/hr/doctype/employee/employee_list.js | 2 +- .../hr/doctype/employee_grievance/__init__.py | 0 .../employee_grievance/employee_grievance.js | 39 +++ .../employee_grievance.json | 261 ++++++++++++++++++ .../employee_grievance/employee_grievance.py | 15 + .../employee_grievance_list.js | 12 + .../test_employee_grievance.py | 51 ++++ erpnext/hr/doctype/grievance_type/__init__.py | 0 .../doctype/grievance_type/grievance_type.js | 8 + .../grievance_type/grievance_type.json | 70 +++++ .../doctype/grievance_type/grievance_type.py | 8 + .../grievance_type/test_grievance_type.py | 8 + erpnext/hr/workspace/hr/hr.json | 20 +- .../additional_salary/additional_salary.js | 17 +- .../doctype/salary_slip/test_salary_slip.py | 1 + .../salary_structure/test_salary_structure.py | 4 +- 20 files changed, 516 insertions(+), 16 deletions(-) create mode 100644 erpnext/hr/doctype/employee_grievance/__init__.py create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.js create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.json create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.py create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance_list.js create mode 100644 erpnext/hr/doctype/employee_grievance/test_employee_grievance.py create mode 100644 erpnext/hr/doctype/grievance_type/__init__.py create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.js create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.json create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.py create mode 100644 erpnext/hr/doctype/grievance_type/test_grievance_type.py diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 4ada8344255..280319321f2 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Employee", ["name", "employee_name"]) return frappe.db.sql("""select {fields} from `tabEmployee` - where status = 'Active' + where status in ('Active', 'Suspended') and docstatus < 2 and ({key} like %(txt)s or employee_name like %(txt)s) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 5442ed56c31..d592a9c79e2 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -207,7 +207,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Active\nInactive\nLeft", + "options": "Active\nInactive\nSuspended\nLeft", "reqd": 1, "search_index": 1 }, @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2021-06-12 11:31:37.730760", + "modified": "2021-06-17 11:31:37.730760", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index bc5694226ad..fa017d9d4c2 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr +from frappe.utils import getdate, validate_email_address, today, add_years, cstr from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ @@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet -from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass @@ -37,7 +36,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Active", "Inactive", "Left"]) + validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"]) self.employee = self.name self.set_employee_name() diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 285374d9f69..e853bee69f2 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -7,7 +7,8 @@ def get_data(): 'heatmap_message': _('This is based on the attendance of this Employee'), 'fieldname': 'employee', 'non_standard_fieldnames': { - 'Bank Account': 'party' + 'Bank Account': 'party', + 'Employee Grievance': 'raised_by' }, 'transactions': [ { @@ -20,7 +21,7 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] + 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] }, { 'label': _('Shift'), diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js index 6679e318c24..d37e1496ca3 100644 --- a/erpnext/hr/doctype/employee/employee_list.js +++ b/erpnext/hr/doctype/employee/employee_list.js @@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = { filters: [["status","=", "Active"]], get_indicator: function(doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; + indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status]; return indicator; } }; diff --git a/erpnext/hr/doctype/employee_grievance/__init__.py b/erpnext/hr/doctype/employee_grievance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.js b/erpnext/hr/doctype/employee_grievance/employee_grievance.js new file mode 100644 index 00000000000..25c5badbc77 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.js @@ -0,0 +1,39 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Grievance', { + setup: function(frm) { + frm.set_query('grievance_against_party', function() { + return { + filters: { + name: ['in', [ + 'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee'] + ] + } + }; + }); + frm.set_query('associated_document_type', function() { + let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website", + "Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"]; + return { + filters: { + istable: 0, + issingle: 0, + module: ["Not In", ignore_modules] + } + }; + }); + }, + + grievance_against_party: function(frm) { + let filters = {}; + if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) { + filters.name = ["!=", frm.doc.raised_by]; + } + frm.set_query('grievance_against', function() { + return { + filters: filters + }; + }); + }, +}); diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.json b/erpnext/hr/doctype/employee_grievance/employee_grievance.json new file mode 100644 index 00000000000..5a918562afb --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.json @@ -0,0 +1,261 @@ +{ + "actions": [], + "autoname": "HR-GRIEV-.YYYY.-.#####", + "creation": "2021-05-11 13:41:51.485295", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "raised_by", + "employee_name", + "designation", + "column_break_3", + "date", + "status", + "reports_to", + "grievance_details_section", + "grievance_against_party", + "grievance_against", + "grievance_type", + "column_break_11", + "associated_document_type", + "associated_document", + "section_break_14", + "description", + "investigation_details_section", + "cause_of_grievance", + "resolution_details_section", + "resolved_by", + "resolution_date", + "employee_responsible", + "column_break_16", + "resolution_detail", + "amended_from" + ], + "fields": [ + { + "fieldname": "grievance_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Type", + "options": "Grievance Type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date ", + "reqd": 1 + }, + { + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Open\nInvestigated\nResolved\nInvalid", + "reqd": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "cause_of_grievance", + "fieldtype": "Text", + "label": "Cause of Grievance", + "mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\"" + }, + { + "fieldname": "resolution_details_section", + "fieldtype": "Section Break", + "label": "Resolution Details" + }, + { + "fieldname": "resolved_by", + "fieldtype": "Link", + "label": "Resolved By", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"", + "options": "User" + }, + { + "fieldname": "employee_responsible", + "fieldtype": "Link", + "label": "Employee Responsible ", + "options": "Employee" + }, + { + "fieldname": "resolution_detail", + "fieldtype": "Small Text", + "label": "Resolution Details", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "resolution_date", + "fieldtype": "Date", + "label": "Resolution Date", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "grievance_against", + "fieldtype": "Dynamic Link", + "label": "Grievance Against", + "options": "grievance_against_party", + "reqd": 1 + }, + { + "fieldname": "raised_by", + "fieldtype": "Link", + "label": "Raised By", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Grievance", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "raised_by.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fetch_from": "raised_by.reports_to", + "fieldname": "reports_to", + "fieldtype": "Link", + "label": "Reports To", + "options": "Employee", + "read_only": 1 + }, + { + "fieldname": "grievance_details_section", + "fieldtype": "Section Break", + "label": "Grievance Details" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Section Break" + }, + { + "fieldname": "grievance_against_party", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Against Party", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "associated_document_type", + "fieldtype": "Link", + "label": "Associated Document Type", + "options": "DocType" + }, + { + "fieldname": "associated_document", + "fieldtype": "Dynamic Link", + "label": "Associated Document", + "options": "associated_document_type" + }, + { + "fieldname": "investigation_details_section", + "fieldtype": "Section Break", + "label": "Investigation Details" + }, + { + "fetch_from": "raised_by.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-06-21 12:51:01.499486", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Grievance", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "search_fields": "subject,raised_by,grievance_against_party", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "subject", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py new file mode 100644 index 00000000000..503b5ea4449 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, bold +from frappe.model.document import Document + +class EmployeeGrievance(Document): + def on_submit(self): + if self.status not in ["Invalid", "Resolved"]: + frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format( + bold("Invalid"), + bold("Resolved")) + ) + diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js new file mode 100644 index 00000000000..fc08e216099 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Employee Grievance"] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + var colors = { + "Open": "red", + "Investigated": "orange", + "Resolved": "green", + "Invalid": "grey" + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py new file mode 100644 index 00000000000..a615b20a5a2 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py @@ -0,0 +1,51 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +import unittest +from frappe.utils import today +from erpnext.hr.doctype.employee.test_employee import make_employee +class TestEmployeeGrievance(unittest.TestCase): + def test_create_employee_grievance(self): + create_employee_grievance() + +def create_employee_grievance(): + grievance_type = create_grievance_type() + emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company") + emp_2 = make_employee("testculprit@example.com", company="_Test Company") + + grievance = frappe.new_doc("Employee Grievance") + grievance.subject = "Test Employee Grievance" + grievance.raised_by = emp_1 + grievance.date = today() + grievance.grievance_type = grievance_type + grievance.grievance_against_party = "Employee" + grievance.grievance_against = emp_2 + grievance.description = "test descrip" + + #set cause + grievance.cause_of_grievance = "test cause" + + #resolution details + grievance.resolution_date = today() + grievance.resolution_detail = "test resolution detail" + grievance.resolved_by = "test_emp_grievance_@example.com" + grievance.employee_responsible = emp_2 + grievance.status = "Resolved" + + grievance.save() + grievance.submit() + + return grievance + + +def create_grievance_type(): + if frappe.db.exists("Grievance Type", "Employee Abuse"): + return frappe.get_doc("Grievance Type", "Employee Abuse") + grievance_type = frappe.new_doc("Grievance Type") + grievance_type.name = "Employee Abuse" + grievance_type.description = "Test" + grievance_type.save() + + return grievance_type.name + diff --git a/erpnext/hr/doctype/grievance_type/__init__.py b/erpnext/hr/doctype/grievance_type/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.js b/erpnext/hr/doctype/grievance_type/grievance_type.js new file mode 100644 index 00000000000..425f2fd5b5e --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Grievance Type', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.json b/erpnext/hr/doctype/grievance_type/grievance_type.json new file mode 100644 index 00000000000..1dce00a0e22 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-05-11 12:41:50.256071", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "description" + ], + "fields": [ + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-06-21 12:54:37.764712", + "modified_by": "Administrator", + "module": "HR", + "name": "Grievance Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.py b/erpnext/hr/doctype/grievance_type/grievance_type.py new file mode 100644 index 00000000000..618cf0a0318 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class GrievanceType(Document): + pass diff --git a/erpnext/hr/doctype/grievance_type/test_grievance_type.py b/erpnext/hr/doctype/grievance_type/test_grievance_type.py new file mode 100644 index 00000000000..a02a34d41fa --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/test_grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestGrievanceType(unittest.TestCase): + pass diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index c5201c22c9b..4500ba4560c 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -153,6 +153,24 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grievance Type", + "link_to": "Grievance Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grievance", + "link_to": "Employee Grievance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Employee", "hidden": 0, @@ -823,7 +841,7 @@ "type": "Link" } ], - "modified": "2021-04-26 13:36:15.413819", + "modified": "2021-05-13 17:19:40.524444", "modified_by": "Administrator", "module": "HR", "name": "HR", diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d1ed91fac70..24ffce537c1 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', { } }; }); + }, - frm.trigger('set_earning_component'); + onload: function(frm) { + if (frm.doc.type) { + frm.trigger('set_component_query'); + } }, employee: function(frm) { @@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', { }, company: function(frm) { - frm.trigger('set_earning_component'); + frm.set_value("type", ""); + frm.trigger('set_component_query'); }, - set_earning_component: function(frm) { + set_component_query: function(frm) { if (!frm.doc.company) return; + let filters = {company: frm.doc.company}; + if (frm.doc.type) { + filters.type = frm.doc.type; + } frm.set_query("salary_component", function() { return { - filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} + filters: filters }; }); }, diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 9e7db977ab1..ce88cc3f1e1 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", {"user_id": user}) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index dce6b7aa3d4..e7d123c9960 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, "doctype": "Salary Structure", "name": salary_structure, "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), + "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, "payment_account": get_random("Account", filters={'account_currency': currency}), "currency": currency From 0354ec6363fdc914b2156d67362c720a0f6af1e1 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:27:45 +0530 Subject: [PATCH 176/376] fix: job applicant link issue (#25933) Co-authored-by: Rucha Mahabal --- erpnext/hr/doctype/job_applicant/job_applicant_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js index 3b9141ba79c..2ad0d591d8c 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_list.js +++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js @@ -2,7 +2,7 @@ // MIT License. See license.txt frappe.listview_settings['Job Applicant'] = { - add_fields: ["company", "designation", "job_applicant", "status"], + add_fields: ["status"], get_indicator: function (doc) { if (doc.status == "Accepted") { return [__(doc.status), "green", "status,=," + doc.status]; From 44815393b375b0a97d461b93595133262fbcb499 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:28:02 +0530 Subject: [PATCH 177/376] fix: job applicant link issue (#25934) --- erpnext/hr/doctype/job_applicant/job_applicant_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js index 3b9141ba79c..2ad0d591d8c 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_list.js +++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js @@ -2,7 +2,7 @@ // MIT License. See license.txt frappe.listview_settings['Job Applicant'] = { - add_fields: ["company", "designation", "job_applicant", "status"], + add_fields: ["status"], get_indicator: function (doc) { if (doc.status == "Accepted") { return [__(doc.status), "green", "status,=," + doc.status]; From d021db1cb14352d1f91e5e2ab36aa5c01ccc9a77 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:59:43 +0530 Subject: [PATCH 178/376] fix: Training event (#26164) Co-authored-by: Rucha Mahabal --- .../training_scheduled.json | 4 ++-- .../training_scheduled/training_scheduled.md | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json index e49541e3214..f3650038fd6 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.json +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json @@ -11,8 +11,8 @@ "event": "Submit", "idx": 0, "is_standard": 1, - "message": "\n \n \n \n \n \n \n \n
    \n
    \n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
    \n
    \n\n\n \n \n \n \n \n \n \n
    \n
    \n {{ doc.introduction }}\n
      \n
    • {{_(\"Event Location\")}}: {{ doc.location }}
    • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
    • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
    • \n
    • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
    • \n {% else %}\n
    • {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
    • \n
    • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
    • \n {% endif %}\n
    • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
    • \n {% if doc.is_mandatory %}\n
    • Note: This Training Event is mandatory
    • \n {% endif %}\n
    \n
    \n
    ", - "modified": "2021-05-24 16:29:13.165930", + "message": "\n \n \n \n \n \n \n \n
    \n
    \n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
    \n
    \n\n\n \n \n \n \n \n \n \n
    \n
    \n {{ doc.introduction }}\n
      \n
    • {{_(\"Event Location\")}}: {{ doc.location }}
    • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
    • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
    • \n
    • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
    • \n {% else %}\n
    • \n {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
    • \n
    • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}
    • \n {% endif %}\n
    • {{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
    • \n {% if doc.is_mandatory %}\n
    • {{ _(\"Note: This Training Event is mandatory\") }}
    • \n {% endif %}\n
    \n
    \n
    ", + "modified": "2021-06-16 14:08:12.933367", "modified_by": "Administrator", "module": "HR", "name": "Training Scheduled", diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md index 418fd4990e3..b9ba846be56 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.md +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md @@ -24,19 +24,19 @@ {% set start = frappe.utils.get_datetime(doc.start_time) %} {% set end = frappe.utils.get_datetime(doc.end_time) %} {% if start.date() == end.date() %} -
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • -
  • - {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} -
  • +
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • +
  • + {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} +
  • {% else %} -
  • {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} -
  • -
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }} -
  • +
  • + {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • +
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }}
  • {% endif %} -
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
  • {{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • {% if doc.is_mandatory %} -
  • Note: This Training Event is mandatory
  • +
  • {{ _("Note: This Training Event is mandatory") }}
  • {% endif %}
    @@ -44,4 +44,4 @@ - \ No newline at end of file + From 80399802c62794f5002937eb291525e3934de5fc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 13:26:47 +0530 Subject: [PATCH 179/376] fix(Asset Repair): Replace asset_value with value_after_depreciation in tests --- .../doctype/asset_repair/test_asset_repair.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index d1b417fd384..52a960e8509 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -74,21 +74,21 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) def test_increase_in_asset_value_due_to_stock_consumption(self): - asset = create_asset() - initial_asset_value = asset.asset_value + asset = create_asset(calculate_depreciation = 1) + initial_asset_value = get_asset_value(asset) asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1) asset.reload() - increase_in_asset_value = asset.asset_value - initial_asset_value + increase_in_asset_value = get_asset_value(asset) - initial_asset_value self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): - asset = create_asset() - initial_asset_value = asset.asset_value + asset = create_asset(calculate_depreciation = 1) + initial_asset_value = get_asset_value(asset) asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) asset.reload() - increase_in_asset_value = asset.asset_value - initial_asset_value + increase_in_asset_value = get_asset_value(asset) - initial_asset_value self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) def test_purchase_invoice(self): @@ -109,6 +109,9 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation) +def get_asset_value(asset): + return asset.finance_books[0].value_after_depreciation + def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations From da66cefefc9c5f249c78f7ecf04e9a11719d8fcf Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:04:48 +0530 Subject: [PATCH 180/376] fix: staffing plan vacancies data type issue (#25936) * fix: staffing plan vacancies data type issue * fix: translation issue * fix: removed greater than 0 condition * fix: sider Co-authored-by: Rucha Mahabal --- .../hr/doctype/staffing_plan/staffing_plan.js | 2 +- .../hr/doctype/staffing_plan/staffing_plan.py | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.js b/erpnext/hr/doctype/staffing_plan/staffing_plan.js index 04af2323c72..228391ba001 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.js +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.js @@ -103,4 +103,4 @@ var set_total_estimated_budget = function(frm) { }) frm.set_value('total_estimated_budget', estimated_budget); } -} \ No newline at end of file +}; diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 533149a8235..e6c783aca22 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -41,7 +41,7 @@ class StaffingPlan(Document): detail.total_estimated_cost = 0 if detail.number_of_positions > 0: - if detail.vacancies > 0 and detail.estimated_cost_per_position: + if detail.vacancies and detail.estimated_cost_per_position: detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) self.total_estimated_budget += detail.total_estimated_cost @@ -76,12 +76,12 @@ class StaffingPlan(Document): if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ - for {2} as per staffing plan {3} for parent company {4}." - .format(cint(parent_plan_details[0].vacancies), + for {2} as per staffing plan {3} for parent company {4}.").format( + cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_plan_details[0].name, - parent_company)), ParentCompanyError) + parent_company), ParentCompanyError) #Get vacanices already planned for all companies down the hierarchy of Parent Company lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) @@ -98,14 +98,14 @@ class StaffingPlan(Document): (flt(parent_plan_details[0].total_estimated_cost) < \ (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ - You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." - .format(cint(all_sibling_details.vacancies), + You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format( + cint(all_sibling_details.vacancies), all_sibling_details.total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_company, cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, - parent_plan_details[0].name))) + parent_plan_details[0].name)) def validate_with_subsidiary_plans(self, staffing_plan_detail): #Valdate this plan with all child company plan @@ -121,11 +121,11 @@ class StaffingPlan(Document): cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ - Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" - .format(self.company, + Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format( + self.company, cint(children_details.vacancies), children_details.total_estimated_cost, - frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) + frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError) @frappe.whitelist() def get_designation_counts(designation, company): @@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now designation, from_date, to_date) # Only a single staffing plan can be active for a designation on given date - return staffing_plan if staffing_plan else None \ No newline at end of file + return staffing_plan if staffing_plan else None From 9ec0ded28ff5e2aea03d6b015e272c9d69209792 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:05:10 +0530 Subject: [PATCH 181/376] fix: Staffing plan vacancies data type issue (#25941) * fix: staffing plan vacancies data type issue * fix: translation issue * fix: removed greater than 0 condition --- .../hr/doctype/staffing_plan/staffing_plan.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 533149a8235..e6c783aca22 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -41,7 +41,7 @@ class StaffingPlan(Document): detail.total_estimated_cost = 0 if detail.number_of_positions > 0: - if detail.vacancies > 0 and detail.estimated_cost_per_position: + if detail.vacancies and detail.estimated_cost_per_position: detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) self.total_estimated_budget += detail.total_estimated_cost @@ -76,12 +76,12 @@ class StaffingPlan(Document): if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ - for {2} as per staffing plan {3} for parent company {4}." - .format(cint(parent_plan_details[0].vacancies), + for {2} as per staffing plan {3} for parent company {4}.").format( + cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_plan_details[0].name, - parent_company)), ParentCompanyError) + parent_company), ParentCompanyError) #Get vacanices already planned for all companies down the hierarchy of Parent Company lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) @@ -98,14 +98,14 @@ class StaffingPlan(Document): (flt(parent_plan_details[0].total_estimated_cost) < \ (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ - You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." - .format(cint(all_sibling_details.vacancies), + You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format( + cint(all_sibling_details.vacancies), all_sibling_details.total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_company, cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, - parent_plan_details[0].name))) + parent_plan_details[0].name)) def validate_with_subsidiary_plans(self, staffing_plan_detail): #Valdate this plan with all child company plan @@ -121,11 +121,11 @@ class StaffingPlan(Document): cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ - Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" - .format(self.company, + Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format( + self.company, cint(children_details.vacancies), children_details.total_estimated_cost, - frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) + frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError) @frappe.whitelist() def get_designation_counts(designation, company): @@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now designation, from_date, to_date) # Only a single staffing plan can be active for a designation on given date - return staffing_plan if staffing_plan else None \ No newline at end of file + return staffing_plan if staffing_plan else None From d802d7397313a857fca97c8f7f7a190a11fa5e5f Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 14:08:07 +0530 Subject: [PATCH 182/376] fix: Consider Website Item Groups in Item group page product listing - Passed an argument to query engine to know when query is for item group page - If for item group page, get data with regards to website item group table - This query should be fast since there's one filter and that shortens the table beforehand - This data is merged with the results from the Item master (results only considering item attributes and field filters) - The combined data is then sorted as per weightage Co-authored-by: Gavin D'souza --- .../setup/doctype/item_group/item_group.py | 2 +- erpnext/shopping_cart/product_query.py | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index db31d6d6992..1c72cebfa9d 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): field_filters['item_group'] = self.name engine = ProductQuery() - context.items = engine.query(attribute_filters, field_filters, search, start) + context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) filter_engine = ProductFiltersBuilder(self.name) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index dd94c26bc60..bb31220447a 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -22,13 +22,14 @@ class ProductQuery: self.settings = frappe.get_doc("Products Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route'] + self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', + 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage'] self.filters = [] self.or_filters = [['show_in_website', '=', 1]] if not self.settings.get('hide_variants'): self.or_filters.append(['show_variant_in_website', '=', 1]) - def query(self, attributes=None, fields=None, search_term=None, start=0): + def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """Summary Args: @@ -44,6 +45,15 @@ class ProductQuery: if search_term: self.build_search_filters(search_term) result = [] + website_item_groups = [] + + # if from item group page consider website item group table + if item_group: + website_item_groups = frappe.db.get_all( + "Item", + fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], + filters=[["Website Item Group", "item_group", "=", item_group]] + ) if attributes: all_items = [] @@ -61,12 +71,10 @@ class ProductQuery: ], or_filters=self.or_filters, start=start, - limit=self.page_length, - order_by="weightage desc" + limit=self.page_length ) items_dict = {item.name: item for item in items} - # TODO: Replace Variants by their parent templates all_items.append(set(items_dict.keys())) @@ -78,14 +86,22 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, start=start, - limit=self.page_length, - order_by="weightage desc" + limit=self.page_length ) + # Combine results having context of website item groups into item results + if item_group and website_item_groups: + items_list = {row.name for row in result} + for row in website_item_groups: + if row.wig_parent not in items_list: + result.append(row) + + result = sorted(result, key=lambda x: x.get("weightage"), reverse=True) + for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None + item.formatted_price = product_info.get('price', {}).get('formatted_price') return result From 1b9b72d12eac988f03b1feda17f6524c96ab5b72 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 16:03:24 +0530 Subject: [PATCH 183/376] fix: Filters did not consider Website Item Group --- erpnext/shopping_cart/filters.py | 21 +++++++++++++++------ erpnext/shopping_cart/product_query.py | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 6c63d8759b4..979afd3c13d 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -22,12 +22,15 @@ class ProductFiltersBuilder: filter_data = [] for df in fields: - filters = {} + filters, or_filters = {}, [] if df.fieldtype == "Link": if self.item_group: - filters['item_group'] = self.item_group + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() @@ -44,7 +47,9 @@ class ProductFiltersBuilder: values = [d.name for d in frappe.get_all(doctype, filters)] # Remove None - values = values.remove(None) if None in values else values + if None in values: + values.remove(None) + if values: filter_data.append([df, values]) @@ -61,14 +66,18 @@ class ProductFiltersBuilder: for attr_doc in attribute_docs: selected_attributes = [] for attr in attr_doc.item_attribute_values: + or_filters = [] filters= [ ["Item Variant Attribute", "attribute", "=", attr.parent], ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] ] if self.item_group: - filters.append(["item_group", "=", self.item_group]) + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - if frappe.db.get_all("Item", filters, limit=1): + if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1): selected_attributes.append(attr) if selected_attributes: diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index bb31220447a..0b05f68ae9b 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -101,7 +101,7 @@ class ProductQuery: for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info.get('price', {}).get('formatted_price') + item.formatted_price = (product_info.get('price') or {}).get('formatted_price') return result From b4b6288596bfff865f05b5a3bba04448a73d200b Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 23 Jun 2021 09:54:12 +0530 Subject: [PATCH 184/376] fix: fetches correct preferred shipping address --- erpnext/accounts/custom/address.py | 2 ++ erpnext/public/js/utils/party.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index 5e764037a74..c417a493c69 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -33,6 +33,8 @@ def get_shipping_address(company, address = None): if address and frappe.db.get_value('Dynamic Link', {'parent': address, 'link_name': company}): filters.append(["Address", "name", "=", address]) + if not address: + filters.append(["Address", "is_shipping_address", "=", 1]) address = frappe.get_all("Address", filters=filters, fields=fields) or {} diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 808dd5add05..99c8587391c 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -274,9 +274,9 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { return true; } -erpnext.utils.get_shipping_address = function(frm, callback){ +erpnext.utils.get_shipping_address = function(frm, callback) { if (frm.doc.company) { - if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + if ((frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || frm.doc.internal_order_reference)) { if (callback) { return callback(); From f913e0dd05b41921d8ad8c6f0fa1620bb1adc545 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:06:11 +0530 Subject: [PATCH 185/376] fix: Consider Table Multiselect fields in Query engine - Since table multiselect fields were not handled, the query tried searching for this child field in item master - This broke the query - On trying to reload or go back to all-products page with field filters that are table mutiselect, page breaks --- erpnext/shopping_cart/product_query.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 0b05f68ae9b..cd4a1769212 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -115,6 +115,17 @@ class ProductQuery: if not values: continue + # handle multiselect fields in filter addition + meta = frappe.get_meta('Item', cached=True) + df = meta.get_field(field) + if df.fieldtype == 'Table MultiSelect': + child_doctype = df.options + child_meta = frappe.get_meta(child_doctype, cached=True) + fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + if fields: + self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) + continue + if isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) From 078826d510aef7d1a1a0e3bce63c451f1d47e727 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:12:59 +0530 Subject: [PATCH 186/376] fix: Sider --- erpnext/shopping_cart/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 979afd3c13d..9f06d20bde5 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() From 1f7b95f39039981fb20e5040d1b9fe68fa78fb59 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 20:56:27 +0530 Subject: [PATCH 187/376] fix: User is not able to change item tax template --- .../public/js/controllers/taxes_and_totals.js | 9 +++++---- erpnext/stock/get_item_details.py | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index e5a5fcfe3b7..4a14a665cda 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -270,11 +270,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ let me = this; let item_codes = []; let item_rates = {}; + let item_tax_templates = {}; + $.each(this.frm.doc.items || [], function(i, item) { if (item.item_code) { // Use combination of name and item code in case same item is added multiple times item_codes.push([item.item_code, item.name]); item_rates[item.name] = item.net_rate; + item_tax_templates[item.name] = item.item_tax_template } }); @@ -285,18 +288,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ company: me.frm.doc.company, tax_category: cstr(me.frm.doc.tax_category), item_codes: item_codes, + item_tax_templates: item_tax_templates, item_rates: item_rates }, callback: function(r) { if (!r.exc) { $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name)) { + if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { item.item_tax_template = r.message[item.name].item_tax_template; item.item_tax_rate = r.message[item.name].item_tax_rate; me.add_taxes_from_item_tax_template(item.item_tax_rate); - } else { - item.item_tax_template = ""; - item.item_tax_rate = "{}"; } }); } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 746cbbf601d..bab004ec92e 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,7 +436,7 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_tax_templates, item_rates=None): out = {} if isinstance(item_codes, string_types): item_codes = json.loads(item_codes) @@ -444,12 +444,18 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None): if isinstance(item_rates, string_types): item_rates = json.loads(item_rates) + if isinstance(item_tax_templates, string_types): + item_tax_templates = json.loads(item_tax_templates) + for item_code in item_codes: - if not item_code or item_code[1] in out: + if not item_code or item_code[1] in out or not item_tax_templates.get(item_code[1]): continue + out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]], + "item_tax_template": item_tax_templates.get(item_code[1])} + get_item_tax_template(args, item, out[item_code[1]]) out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) @@ -463,9 +469,7 @@ def get_item_tax_template(args, item, out): } """ item_tax_template = args.get("item_tax_template") - - if not item_tax_template: - item_tax_template = _get_item_tax_template(args, item.taxes, out) + item_tax_template = _get_item_tax_template(args, item.taxes, out) if not item_tax_template: item_group = item.item_group @@ -508,7 +512,8 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return None # do not change if already a valid template - if args.get('item_tax_template') in taxes: + if args.get('item_tax_template') in [t.item_tax_template for t in taxes]: + out["item_tax_template"] = args.get('item_tax_template') return args.get('item_tax_template') for tax in taxes: From 2f5f9d8566a57fd3b9781cfbabfe71583dafea62 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 22:19:05 +0530 Subject: [PATCH 188/376] fix(Asset): Fix value_after_depreciation calculation --- erpnext/assets/doctype/asset/asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 27d21e25420..29379657a10 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -182,10 +182,10 @@ class Asset(AccountsController): # value_after_depreciation - current Asset value if d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + flt(self.opening_accumulated_depreciation)) else: value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + flt(self.opening_accumulated_depreciation)) d.value_after_depreciation = value_after_depreciation From dce21137364b86e997a2d000c00c25b4d2c78684 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 22:26:45 +0530 Subject: [PATCH 189/376] fix(Asset Repair): Remove test that's no longer necessary --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 52a960e8509..b3d78b3bfbb 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -13,12 +13,6 @@ class TestAssetRepair(unittest.TestCase): create_asset_data() frappe.db.sql("delete from `tabTax Rule`") - def test_completion_date(self): - asset_repair = create_asset_repair() - asset_repair.repair_status = "Completed" - asset_repair.save() - self.assertTrue(asset_repair.completion_date) - def test_update_status(self): asset = create_asset() initial_status = asset.status From c9c1d10435a327db4b19c4529802a01aa19ccf31 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 22:47:29 +0530 Subject: [PATCH 190/376] fix: Make item tax templates optional --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bab004ec92e..773a18fbf92 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,7 +436,7 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_tax_templates, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None, item_rates=None): out = {} if isinstance(item_codes, string_types): item_codes = json.loads(item_codes) From 7e006496dd199c5c46e2e9cc77f2868583c53d16 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 22:52:51 +0530 Subject: [PATCH 191/376] fix: Check if item tax template exists --- erpnext/stock/get_item_details.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 773a18fbf92..37850350abb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -453,8 +453,10 @@ def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]], - "item_tax_template": item_tax_templates.get(item_code[1])} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + + if item_tax_templates: + args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) get_item_tax_template(args, item, out[item_code[1]]) out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) From 53fefd751338676d1b33d1bdbfce298cebade785 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 24 Jun 2021 10:09:02 +0530 Subject: [PATCH 192/376] feat: fetching of qty as per received qty from PR to PI (#25837) --- .../doctype/buying_settings/buying_settings.json | 14 +++++++++++--- erpnext/controllers/accounts_controller.py | 10 ++++++++-- erpnext/patches.txt | 1 + ...ll_for_rejected_quantity_in_purchase_invoice.py | 8 ++++++++ .../doctype/purchase_receipt/purchase_receipt.py | 8 ++++++-- .../purchase_receipt/test_purchase_receipt.py | 11 +++++++++-- 6 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 630a1dc8cd5..838a9abf8c0 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -9,13 +9,14 @@ "supp_master_name", "supplier_group", "buying_price_list", + "maintain_same_rate_action", + "role_to_override_stop_action", "column_break_3", "po_required", "pr_required", "maintain_same_rate", - "maintain_same_rate_action", - "role_to_override_stop_action", "allow_multiple_items", + "bill_for_rejected_quantity_in_purchase_invoice", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -108,6 +109,13 @@ "fieldtype": "Link", "label": "Role Allowed to Override Stop Action", "options": "Role" + }, + { + "default": "1", + "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", + "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", + "fieldtype": "Check", + "label": "Bill for Rejected Quantity in Purchase Invoice" } ], "icon": "fa fa-cog", @@ -115,7 +123,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-04 20:01:44.087066", + "modified": "2021-06-23 19:40:00.120822", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 243939b275f..1c086e9edcd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -828,8 +828,14 @@ class AccountsController(TransactionBase): role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles(): - frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") - .format(item.item_code, item.idx, max_allowed_amt)) + if self.doctype != "Purchase Invoice": + self.throw_overbill_exception(item, max_allowed_amt) + elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")): + self.throw_overbill_exception(item, max_allowed_amt) + + def throw_overbill_exception(self, item, max_allowed_amt): + frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") + .format(item.item_code, item.idx, max_allowed_amt)) def get_company_default(self, fieldname): from erpnext.accounts.utils import get_company_default diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 95cdc308a7c..339e7f99d26 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -288,4 +288,5 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 erpnext.patches.v13_0.set_training_event_attendance +erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py new file mode 100644 index 00000000000..7de9fa1e23e --- /dev/null +++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doctype("Buying Settings") + buying_settings = frappe.get_single("Buying Settings") + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0 + buying_settings.save() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b8580f95a3a..e488b695b5f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -581,7 +581,6 @@ def update_billing_percentage(pr_doc, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): - from frappe.model.mapper import get_mapped_doc from erpnext.accounts.party import get_payment_terms_template doc = frappe.get_doc('Purchase Receipt', source_name) @@ -601,11 +600,16 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + target_doc.rejected_qty = 0 target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): - pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) + qty = item_row.qty + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + qty = item_row.received_qty + pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = flt(returned_qty_map.get(item_row.name, 0)) if returned_qty: if returned_qty >= pending_qty: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 95096d77d77..99abf3a68cf 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -421,11 +421,18 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(return_pr_2.items[0].qty, -3) # Make PI against unreturned amount + buying_settings = frappe.get_single("Buying Settings") + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0 + buying_settings.save() + pi = make_purchase_invoice(pr.name) pi.submit() self.assertEqual(pi.items[0].qty, 3) + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 1 + buying_settings.save() + pr.load_from_db() # PR should be completed on billing all unreturned amount self.assertEqual(pr.items[0].billed_amt, 150) @@ -767,8 +774,8 @@ class TestPurchaseReceipt(unittest.TestCase): pr1.items[0].purchase_receipt_item = pr.items[0].name pr1.submit() - pi = make_purchase_invoice(pr.name) - self.assertEqual(pi.items[0].qty, 3) + pi1 = make_purchase_invoice(pr.name) + self.assertEqual(pi1.items[0].qty, 3) pr1.cancel() pr.reload() From 3da0541000f772a8e4ec45ff9e5359be9c52e796 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Jun 2021 22:14:29 +0530 Subject: [PATCH 193/376] fix: Accouting Dimensions for payroll entry accrual Journal Entry --- .../doctype/payroll_entry/payroll_entry.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 7a70679db47..697d2f61670 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -11,6 +11,7 @@ from frappe import _ from erpnext.accounts.utils import get_fiscal_year from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from frappe.desk.reportview import get_match_cond, get_filters_cond +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions class PayrollEntry(Document): def onload(self): @@ -211,7 +212,7 @@ class PayrollEntry(Document): return account_dict def make_accrual_jv_entry(self): - self.check_permission('write') + self.check_permission("write") earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} payroll_payable_account = self.payroll_payable_account @@ -219,12 +220,13 @@ class PayrollEntry(Document): precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") if earnings or deductions: - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Journal Entry' - journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\ + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Journal Entry" + journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\ .format(self.start_date, self.end_date) journal_entry.company = self.company journal_entry.posting_date = self.posting_date + accounting_dimensions = get_accounting_dimensions() or [] accounts = [] currencies = [] @@ -236,37 +238,34 @@ class PayrollEntry(Document): for acc_cc, amount in earnings.items(): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount += flt(amount, precision) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": acc_cc[0], "debit_in_account_currency": flt(amt, precision), "exchange_rate": flt(exchange_rate), - "party_type": '', "cost_center": acc_cc[1] or self.cost_center, "project": self.project - }) + }, accounting_dimensions)) # Deductions for acc_cc, amount in deductions.items(): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount -= flt(amount, precision) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": acc_cc[0], "credit_in_account_currency": flt(amt, precision), "exchange_rate": flt(exchange_rate), "cost_center": acc_cc[1] or self.cost_center, - "party_type": '', "project": self.project - }) + }, accounting_dimensions)) # Payable amount exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": payroll_payable_account, "credit_in_account_currency": flt(payable_amt, precision), "exchange_rate": flt(exchange_rate), - "party_type": '', "cost_center": self.cost_center - }) + }, accounting_dimensions)) journal_entry.set("accounts", accounts) if len(currencies) > 1: @@ -286,6 +285,12 @@ class PayrollEntry(Document): return jv_name + def update_accounting_dimensions(self, row, accounting_dimensions): + for dimension in accounting_dimensions: + row.update({ dimension: self.get(dimension)}) + + return row + def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): conversion_rate = 1 exchange_rate = self.exchange_rate From c451f43f64a7bfda73e705b60dfc455a7830a520 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Jun 2021 11:21:21 +0530 Subject: [PATCH 194/376] fix(minor): Translation and linting issues --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 697d2f61670..e71d81f323a 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -42,7 +42,7 @@ class PayrollEntry(Document): emp_with_sal_slip.append(employee_details.employee) if len(emp_with_sal_slip): - frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip))) + frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` @@ -287,7 +287,7 @@ class PayrollEntry(Document): def update_accounting_dimensions(self, row, accounting_dimensions): for dimension in accounting_dimensions: - row.update({ dimension: self.get(dimension)}) + row.update({dimension: self.get(dimension)}) return row From 70823b6d9a5a8e9655fe05f10cf2ac25362c24ef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Jun 2021 19:58:16 +0530 Subject: [PATCH 195/376] fix: Incorrect billed qty in Sales Order analytics --- .../selling/report/sales_order_analysis/sales_order_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index f5feb95f1a8..8cb24460f7b 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -59,7 +59,7 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, - IFNULL(sii.qty, 0) as billed_qty, + IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, From da7f45130b035ace547b5578733bbb139b618afc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Jun 2021 10:45:35 +0530 Subject: [PATCH 196/376] fix: Billing address not fetched in Purchase Invoice --- .../doctype/purchase_invoice/purchase_invoice.js | 12 ++++++------ erpnext/public/js/controllers/transaction.js | 3 --- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 617b5b49d4c..3e12a43fe1c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -27,12 +27,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. }); } - company() { - erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - } - onload() { - super.onload(); + this._super(); if(!this.frm.doc.__islocal) { // show credit_to in print format @@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", { frm: frm, freeze_message: __("Creating Purchase Receipt ...") }) - } + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, }) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 210237fbde5..8360337ef73 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -888,9 +888,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } - if (this.frm.doc.posting_date) var date = this.frm.doc.posting_date; - else var date = this.frm.doc.transaction_date; - if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ From 7bef18f3d821d3ad5dfb69544aced7887c7f3417 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 10:28:25 +0530 Subject: [PATCH 197/376] fix: Remove unintentional changes --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 3e12a43fe1c..7562418fd2f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -28,7 +28,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } onload() { - this._super(); + super.onload(); if(!this.frm.doc.__islocal) { // show credit_to in print format From 2dc90dd8ac3e392de7329deca55cdc02cc3f2ce6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 13:03:22 +0530 Subject: [PATCH 198/376] fix: Export invoices not visible in GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 80e2d725a2d..10961593e1c 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -201,7 +201,7 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ - conditions += " AND billing_address_gstin NOT IN %(company_gstins)s" + conditions += " AND IFNULL(billing_address_gstin, '') NOT IN %(company_gstins)s" return conditions From 94d460412a74fc61f49a9ae30ce6fe76b6e72c46 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 20:56:27 +0530 Subject: [PATCH 199/376] fix: User is not able to change item tax template --- .../public/js/controllers/taxes_and_totals.js | 9 +++++---- erpnext/stock/get_item_details.py | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index cc33b8bc35a..a25429f76df 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -272,11 +272,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { let me = this; let item_codes = []; let item_rates = {}; + let item_tax_templates = {}; + $.each(this.frm.doc.items || [], function(i, item) { if (item.item_code) { // Use combination of name and item code in case same item is added multiple times item_codes.push([item.item_code, item.name]); item_rates[item.name] = item.net_rate; + item_tax_templates[item.name] = item.item_tax_template } }); @@ -287,18 +290,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { company: me.frm.doc.company, tax_category: cstr(me.frm.doc.tax_category), item_codes: item_codes, + item_tax_templates: item_tax_templates, item_rates: item_rates }, callback: function(r) { if (!r.exc) { $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name)) { + if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { item.item_tax_template = r.message[item.name].item_tax_template; item.item_tax_rate = r.message[item.name].item_tax_rate; me.add_taxes_from_item_tax_template(item.item_tax_rate); - } else { - item.item_tax_template = ""; - item.item_tax_rate = "{}"; } }); } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 746cbbf601d..bab004ec92e 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,7 +436,7 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_tax_templates, item_rates=None): out = {} if isinstance(item_codes, string_types): item_codes = json.loads(item_codes) @@ -444,12 +444,18 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None): if isinstance(item_rates, string_types): item_rates = json.loads(item_rates) + if isinstance(item_tax_templates, string_types): + item_tax_templates = json.loads(item_tax_templates) + for item_code in item_codes: - if not item_code or item_code[1] in out: + if not item_code or item_code[1] in out or not item_tax_templates.get(item_code[1]): continue + out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]], + "item_tax_template": item_tax_templates.get(item_code[1])} + get_item_tax_template(args, item, out[item_code[1]]) out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) @@ -463,9 +469,7 @@ def get_item_tax_template(args, item, out): } """ item_tax_template = args.get("item_tax_template") - - if not item_tax_template: - item_tax_template = _get_item_tax_template(args, item.taxes, out) + item_tax_template = _get_item_tax_template(args, item.taxes, out) if not item_tax_template: item_group = item.item_group @@ -508,7 +512,8 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return None # do not change if already a valid template - if args.get('item_tax_template') in taxes: + if args.get('item_tax_template') in [t.item_tax_template for t in taxes]: + out["item_tax_template"] = args.get('item_tax_template') return args.get('item_tax_template') for tax in taxes: From 808f83f385b201caade6f1cbead19d4d13b6b83f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 22:47:29 +0530 Subject: [PATCH 200/376] fix: Make item tax templates optional --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bab004ec92e..773a18fbf92 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,7 +436,7 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_tax_templates, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None, item_rates=None): out = {} if isinstance(item_codes, string_types): item_codes = json.loads(item_codes) From 269962a8dcc0872eed6557a5627eacfe7383363c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 22:52:51 +0530 Subject: [PATCH 201/376] fix: Check if item tax template exists --- erpnext/stock/get_item_details.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 773a18fbf92..37850350abb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -453,8 +453,10 @@ def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]], - "item_tax_template": item_tax_templates.get(item_code[1])} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + + if item_tax_templates: + args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) get_item_tax_template(args, item, out[item_code[1]]) out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) From 7dbb032468748bdf6278be34ad28485d0c034c04 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 24 Jun 2021 10:41:56 +0530 Subject: [PATCH 202/376] fix: chart not visible for First Response Time reports (#26032) --- .../first_response_time_for_opportunity.js | 7 +++---- .../first_response_time_for_issues.js | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js index 3f5c95ab0aa..fe5707af296 100644 --- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js @@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = { get_chart_data: function (_columns, result) { return { data: { - labels: result.map(d => d[0]), + labels: result.map(d => d.creation_date), datasets: [{ name: "First Response Time", - values: result.map(d => d[1]) + values: result.map(d => d.first_response_time) }] }, type: "line", @@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = { hide_days: 0, hide_seconds: 0 }; - value = frappe.utils.get_formatted_duration(d, duration_options); - return value; + return frappe.utils.get_formatted_duration(d, duration_options); } } } diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js index 576e0b76dad..18691fe264f 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js @@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Issues"] = { get_chart_data: function(_columns, result) { return { data: { - labels: result.map(d => d[0]), + labels: result.map(d => d.creation_date), datasets: [{ name: 'First Response Time', - values: result.map(d => d[1]) + values: result.map(d => d.first_response_time) }] }, type: "line", @@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Issues"] = { hide_days: 0, hide_seconds: 0 }; - value = frappe.utils.get_formatted_duration(d, duration_options); - return value; + return frappe.utils.get_formatted_duration(d, duration_options); } } } From f5fa1ee4b65c5f38c1398da934fdc2cd25a09777 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 11:03:32 +0530 Subject: [PATCH 203/376] fix: Country Link field in 'Add address' website modal auto-clears --- erpnext/templates/includes/cart/cart_address.html | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 84a9430956e..4482bc10cf7 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -99,6 +99,7 @@ frappe.ready(() => { fieldname: 'country', fieldtype: 'Link', options: 'Country', + only_select: true, reqd: 1 }, { From 604f0b8f0183e3889d3a5cfd9899046abcd34833 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Thu, 24 Jun 2021 11:04:27 +0530 Subject: [PATCH 204/376] fix: message on successful creation of opening invoices (#25998) --- .../opening_invoice_creation_tool.js | 10 +++++++++- .../opening_invoice_creation_tool.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index b2e86267c8f..a8c07d6bb9b 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', { doc: frm.doc, btn: $(btn_primary), method: "make_invoices", - freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]) + freeze: 1, + freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]), + callback: function(r) { + if (r.message.length == 1) { + frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type])); + } else if (r.message.length < 50) { + frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type])); + } + } }); }); diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 29dc96e8c6f..d76d9099628 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -216,7 +216,8 @@ def start_import(invoices): return names def publish(index, total, doctype): - if total < 5: return + if total < 50: + return frappe.publish_realtime( "opening_invoice_creation_progress", dict( @@ -241,4 +242,3 @@ def get_temporary_opening_account(company=None): return accounts[0].name - From a9b5dc6030c3a25f65793170f1b8a05b78a3ba9a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 24 Jun 2021 11:53:28 +0530 Subject: [PATCH 205/376] fix: chart not visible for First Response Time reports (#26032) (#26185) --- .../first_response_time_for_opportunity.js | 7 +++---- .../first_response_time_for_issues.js | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js index 3f5c95ab0aa..fe5707af296 100644 --- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js @@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = { get_chart_data: function (_columns, result) { return { data: { - labels: result.map(d => d[0]), + labels: result.map(d => d.creation_date), datasets: [{ name: "First Response Time", - values: result.map(d => d[1]) + values: result.map(d => d.first_response_time) }] }, type: "line", @@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = { hide_days: 0, hide_seconds: 0 }; - value = frappe.utils.get_formatted_duration(d, duration_options); - return value; + return frappe.utils.get_formatted_duration(d, duration_options); } } } diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js index 576e0b76dad..18691fe264f 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js @@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Issues"] = { get_chart_data: function(_columns, result) { return { data: { - labels: result.map(d => d[0]), + labels: result.map(d => d.creation_date), datasets: [{ name: 'First Response Time', - values: result.map(d => d[1]) + values: result.map(d => d.first_response_time) }] }, type: "line", @@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Issues"] = { hide_days: 0, hide_seconds: 0 }; - value = frappe.utils.get_formatted_duration(d, duration_options); - return value; + return frappe.utils.get_formatted_duration(d, duration_options); } } } From bbe64b560446e5e812d55a0bb104e0fbb4f2a683 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 12:01:12 +0530 Subject: [PATCH 206/376] fix: (style) Address card buttons hover state --- erpnext/public/scss/shopping_cart.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 9402cf9ea48..5962859be5a 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -467,11 +467,15 @@ body.product-page { .btn-change-address { color: var(--blue-500); - box-shadow: none; - border: 1px solid var(--blue-500); } } +.btn-new-address:hover, .btn-change-address:hover { + box-shadow: none; + color: var(--blue-500) !important; + border: 1px solid var(--blue-500); +} + .modal .address-card { .card-body { padding: var(--padding-sm); From bd9317956beb1ffe4aaefd59cee72f39d9a7ad4f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 19:48:08 +0530 Subject: [PATCH 207/376] fix: Taxes on Internal Transfer payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b6b2bef9633..adaf99a7900 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -706,7 +706,7 @@ class PaymentEntry(AccountsController): if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if self.payment_type == 'Pay': + if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" @@ -761,7 +761,7 @@ class PaymentEntry(AccountsController): return self.advance_tax_account elif self.payment_type == 'Receive': return self.paid_from - elif self.payment_type == 'Pay': + elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to def update_advance_paid(self): From 9d8e8f8bdfabbbc8b3721d354bf441098b212933 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 20:38:35 +0530 Subject: [PATCH 208/376] fix: Do not show received amount after tax for internal tarnsfers --- erpnext/accounts/doctype/payment_entry/payment_entry.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 54623dd6cd5..51f18a5a4e3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -690,7 +690,7 @@ "options": "Account" }, { - "depends_on": "eval:doc.received_amount", + "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", "label": "Received Amount After Tax", @@ -707,7 +707,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-09 11:55:04.215050", + "modified": "2021-06-22 20:37:06.154206", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 60a44ae1e63dafa8d9b15b1d11a479f74c196a73 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 12:44:13 +0530 Subject: [PATCH 209/376] fix(Asset): Fix test --- erpnext/assets/doctype/asset/test_asset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index f3667c7b95f..32bdb5224a7 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": "2030-12-31" }) - asset.insert() self.assertEqual(asset.status, "Draft") asset.save() expected_schedules = [ From a4f5dcaa9ab610c10de24a1bc0c835cd615d6a09 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 22:38:10 +0530 Subject: [PATCH 210/376] chore: Test for Item visibility in multiple item group pages --- .../test_product_configurator.py | 63 +++++++++++++++++++ erpnext/shopping_cart/filters.py | 2 +- erpnext/shopping_cart/product_query.py | 6 +- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index 3521e7e8bf0..daaba671736 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -43,6 +43,30 @@ class TestProductConfigurator(unittest.TestCase): "show_variant_in_website": 1 }).insert() + def create_regular_web_item(self, name, item_group=None): + if not frappe.db.exists('Item', name): + doc = frappe.get_doc({ + "description": name, + "item_code": name, + "item_name": name, + "doctype": "Item", + "is_stock_item": 1, + "item_group": item_group or "_Test Item Group", + "stock_uom": "_Test UOM", + "item_defaults": [{ + "company": "_Test Company", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + "income_account": "Sales - _TC" + }], + "show_in_website": 1 + }).insert() + else: + doc = frappe.get_doc("Item", name) + return doc + def test_product_list(self): template_items = frappe.get_all('Item', {'show_in_website': 1}) variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) @@ -79,3 +103,42 @@ class TestProductConfigurator(unittest.TestCase): 'Test Size': ['2XL'] }) self.assertEqual(len(items), 1) + + def test_products_in_multiple_item_groups(self): + """Check if product is visible on multiple item group pages barring its own.""" + from erpnext.shopping_cart.product_query import ProductQuery + + if not frappe.db.exists("Item Group", {"name": "Tech Items"}): + item_group_doc = frappe.get_doc({ + "doctype": "Item Group", + "item_group_name": "Tech Items", + "parent_item_group": "All Item Groups", + "show_in_website": 1 + }).insert() + else: + item_group_doc = frappe.get_doc("Item Group", "Tech Items") + + doc = self.create_regular_web_item("Portal Item", item_group="Tech Items") + if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}): + doc.append("website_item_groups", { + "item_group": "_Test Item Group Desktops" + }) + doc.save() + + # check if item is visible in its own Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") + self.assertEqual(len(items), 1) + self.assertEqual(items[0].item_code, "Portal Item") + + # check if item is visible in configured foreign Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") + item_codes = [row.item_code for row in items] + + self.assertIn(len(items), [2, 3]) + self.assertIn("Portal Item", item_codes) + + # teardown + doc.delete() + item_group_doc.delete() \ No newline at end of file diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 9f06d20bde5..7dfa09e2d62 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname) else: doctype = df.get_link_doctype() diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index cd4a1769212..d96d803416c 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -121,12 +121,10 @@ class ProductQuery: if df.fieldtype == 'Table MultiSelect': child_doctype = df.options child_meta = frappe.get_meta(child_doctype, cached=True) - fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + fields = child_meta.get("fields") if fields: self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) - continue - - if isinstance(values, list): + elif isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) else: From ca8c3b90f02d01f1f1fa8652d8f3d89125ef394b Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 24 Jun 2021 13:13:03 +0530 Subject: [PATCH 211/376] fix: Update leave allocation after submit (#25975) * update leave allocation after submit * fix: leave allocation update after submit * fix: circular dependency * fix: replaced update with submit in tests * fix: replaced update with submit in tests * fix: updating total leaves allocated --- .../leave_allocation/leave_allocation.json | 5 +- .../leave_allocation/leave_allocation.py | 40 ++++++++++++++- .../leave_allocation/test_leave_allocation.py | 49 ++++++++++++++++++- .../employee_leave_balance.py | 2 +- 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index ae02c512c23..ae009ba3e1b 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -110,6 +110,7 @@ "label": "Allocation" }, { + "allow_on_submit": 1, "bold": 1, "fieldname": "new_leaves_allocated", "fieldtype": "Float", @@ -235,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-14 15:28:26.335104", + "modified": "2021-06-03 15:28:26.335104", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -277,4 +278,4 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "employee" -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 11302cad758..4757cd3b192 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry +from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period class OverlapError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass @@ -55,6 +56,43 @@ class LeaveAllocation(Document): if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) + def on_update_after_submit(self): + if self.has_value_changed("new_leaves_allocated"): + self.validate_against_leave_applications() + leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count() + args = { + "leaves": leaves_to_be_added, + "from_date": self.from_date, + "to_date": self.to_date, + "is_carry_forward": 0 + } + create_leave_ledger_entry(self, args, True) + + def get_existing_leave_count(self): + ledger_entries = frappe.get_all("Leave Ledger Entry", + filters={ + "transaction_type": "Leave Allocation", + "transaction_name": self.name, + "employee": self.employee, + "company": self.company, + "leave_type": self.leave_type + }, + pluck="leaves") + total_existing_leaves = 0 + for entry in ledger_entries: + total_existing_leaves += entry + + return total_existing_leaves + + def validate_against_leave_applications(self): + leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, + self.from_date, self.to_date) + if flt(leaves_taken) > flt(self.total_leaves_allocated): + if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): + frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) + else: + frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) + def update_leave_policy_assignments_when_no_allocations_left(self): allocations = frappe.db.get_list("Leave Allocation", filters = { "docstatus": 1, @@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date): def validate_carry_forward(leave_type): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): - frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) \ No newline at end of file + frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 6e7ae87d08c..49dd701a1e5 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals import frappe +import erpnext import unittest from frappe.utils import nowdate, add_months, getdate, add_days from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation - class TestLeaveAllocation(unittest.TestCase): @classmethod def setUpClass(cls): @@ -164,6 +164,53 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.cancel() self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) + + def test_leave_addition_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 40 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 40) + + def test_leave_subtraction_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 10 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 10) + + def test_against_leave_application_validation_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + leave_application = frappe.get_doc({ + "doctype": 'Leave Application', + "employee": employee.name, + "leave_type": "_Test Leave Type", + "from_date": nowdate(), + "to_date": add_days(nowdate(), 10), + "company": erpnext.get_default_company() or "_Test Company", + "docstatus": 1, + "status": "Approved", + "leave_approver": 'test@example.com' + }) + leave_application.submit() + leave_allocation.new_leaves_allocated = 8 + leave_allocation.total_leaves_allocated = 8 + self.assertRaises(frappe.ValidationError, leave_allocation.submit) + def create_leave_allocation(**args): args = frappe._dict(args) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 4dd4570e8ca..b8953b3eaac 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 AND leaves>0 + AND docstatus=1 AND (from_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s OR (from_date < %(from_date)s AND to_date > %(to_date)s)) From 550fe8ae395e6bf4b5598ed01fa81229fc8955ce Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 13:22:26 +0530 Subject: [PATCH 212/376] fix(Asset): Remove redundant code --- erpnext/assets/doctype/asset/test_asset.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 32bdb5224a7..59fbe3b0301 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -153,9 +153,8 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": '2030-12-31' }) - asset.insert() - self.assertEqual(asset.status, "Draft") asset.save() + self.assertEqual(asset.status, "Draft") expected_schedules = [ ['2030-12-31', 66667.00, 66667.00], @@ -184,7 +183,7 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": "2030-12-31" }) - asset.insert() + asset.save() self.assertEqual(asset.status, "Draft") expected_schedules = [ @@ -215,7 +214,6 @@ class TestAsset(unittest.TestCase): "depreciation_start_date": "2030-12-31" }) - asset.insert() asset.save() expected_schedules = [ @@ -246,7 +244,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() asset.load_from_db() self.assertEqual(asset.status, "Submitted") @@ -349,7 +346,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() post_depreciation_entries(date="2021-01-01") @@ -379,7 +375,6 @@ class TestAsset(unittest.TestCase): "total_number_of_depreciations": 10, "frequency_of_depreciation": 1 }) - asset.insert() asset.submit() post_depreciation_entries(date=add_months('2020-01-01', 4)) @@ -423,7 +418,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() post_depreciation_entries(date="2021-01-01") @@ -467,7 +461,7 @@ class TestAsset(unittest.TestCase): "total_number_of_depreciations": 3, "frequency_of_depreciation": 10 }) - asset.insert() + asset.save() accumulated_depreciation_after_full_schedule = \ max(d.accumulated_depreciation_amount for d in asset.get("schedules")) From 26bec9d7b4e3c6b360f38202b541b4c921c18244 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 11:03:32 +0530 Subject: [PATCH 213/376] fix: Country Link field in 'Add address' website modal auto-clears --- erpnext/templates/includes/cart/cart_address.html | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 84a9430956e..4482bc10cf7 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -99,6 +99,7 @@ frappe.ready(() => { fieldname: 'country', fieldtype: 'Link', options: 'Country', + only_select: true, reqd: 1 }, { From 5884f1aeb02411200b150604d37419eee15c67d8 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 12:01:12 +0530 Subject: [PATCH 214/376] fix: (style) Address card buttons hover state --- erpnext/public/scss/shopping_cart.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 9402cf9ea48..5962859be5a 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -467,11 +467,15 @@ body.product-page { .btn-change-address { color: var(--blue-500); - box-shadow: none; - border: 1px solid var(--blue-500); } } +.btn-new-address:hover, .btn-change-address:hover { + box-shadow: none; + color: var(--blue-500) !important; + border: 1px solid var(--blue-500); +} + .modal .address-card { .card-body { padding: var(--padding-sm); From ea2408744a9a79cd4865a31fa41c0f8cc19a5c86 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 14:08:07 +0530 Subject: [PATCH 215/376] fix: Consider Website Item Groups in Item group page product listing - Passed an argument to query engine to know when query is for item group page - If for item group page, get data with regards to website item group table - This query should be fast since there's one filter and that shortens the table beforehand - This data is merged with the results from the Item master (results only considering item attributes and field filters) - The combined data is then sorted as per weightage Co-authored-by: Gavin D'souza --- .../setup/doctype/item_group/item_group.py | 2 +- erpnext/shopping_cart/product_query.py | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index bff806d5472..668714314f6 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): field_filters['item_group'] = self.name engine = ProductQuery() - context.items = engine.query(attribute_filters, field_filters, search, start) + context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) filter_engine = ProductFiltersBuilder(self.name) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 36d446ed0fd..bb31220447a 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -22,13 +22,14 @@ class ProductQuery: self.settings = frappe.get_doc("Products Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route'] + self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', + 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage'] self.filters = [] self.or_filters = [['show_in_website', '=', 1]] if not self.settings.get('hide_variants'): self.or_filters.append(['show_variant_in_website', '=', 1]) - def query(self, attributes=None, fields=None, search_term=None, start=0): + def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """Summary Args: @@ -44,6 +45,15 @@ class ProductQuery: if search_term: self.build_search_filters(search_term) result = [] + website_item_groups = [] + + # if from item group page consider website item group table + if item_group: + website_item_groups = frappe.db.get_all( + "Item", + fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], + filters=[["Website Item Group", "item_group", "=", item_group]] + ) if attributes: all_items = [] @@ -65,18 +75,33 @@ class ProductQuery: ) items_dict = {item.name: item for item in items} - # TODO: Replace Variants by their parent templates all_items.append(set(items_dict.keys())) result = [items_dict.get(item) for item in list(set.intersection(*all_items))] else: - result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length) + result = frappe.get_all( + "Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + start=start, + limit=self.page_length + ) + + # Combine results having context of website item groups into item results + if item_group and website_item_groups: + items_list = {row.name for row in result} + for row in website_item_groups: + if row.wig_parent not in items_list: + result.append(row) + + result = sorted(result, key=lambda x: x.get("weightage"), reverse=True) for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None + item.formatted_price = product_info.get('price', {}).get('formatted_price') return result From 9f305e983cc301aa628648f7efe66e138b271607 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 16:03:24 +0530 Subject: [PATCH 216/376] fix: Filters did not consider Website Item Group --- erpnext/shopping_cart/filters.py | 21 +++++++++++++++------ erpnext/shopping_cart/product_query.py | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 6c63d8759b4..979afd3c13d 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -22,12 +22,15 @@ class ProductFiltersBuilder: filter_data = [] for df in fields: - filters = {} + filters, or_filters = {}, [] if df.fieldtype == "Link": if self.item_group: - filters['item_group'] = self.item_group + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() @@ -44,7 +47,9 @@ class ProductFiltersBuilder: values = [d.name for d in frappe.get_all(doctype, filters)] # Remove None - values = values.remove(None) if None in values else values + if None in values: + values.remove(None) + if values: filter_data.append([df, values]) @@ -61,14 +66,18 @@ class ProductFiltersBuilder: for attr_doc in attribute_docs: selected_attributes = [] for attr in attr_doc.item_attribute_values: + or_filters = [] filters= [ ["Item Variant Attribute", "attribute", "=", attr.parent], ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] ] if self.item_group: - filters.append(["item_group", "=", self.item_group]) + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - if frappe.db.get_all("Item", filters, limit=1): + if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1): selected_attributes.append(attr) if selected_attributes: diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index bb31220447a..0b05f68ae9b 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -101,7 +101,7 @@ class ProductQuery: for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info.get('price', {}).get('formatted_price') + item.formatted_price = (product_info.get('price') or {}).get('formatted_price') return result From f91383837329e1d82f2a8fdb4a2119fd56efa74d Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:06:11 +0530 Subject: [PATCH 217/376] fix: Consider Table Multiselect fields in Query engine - Since table multiselect fields were not handled, the query tried searching for this child field in item master - This broke the query - On trying to reload or go back to all-products page with field filters that are table mutiselect, page breaks --- erpnext/shopping_cart/product_query.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 0b05f68ae9b..cd4a1769212 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -115,6 +115,17 @@ class ProductQuery: if not values: continue + # handle multiselect fields in filter addition + meta = frappe.get_meta('Item', cached=True) + df = meta.get_field(field) + if df.fieldtype == 'Table MultiSelect': + child_doctype = df.options + child_meta = frappe.get_meta(child_doctype, cached=True) + fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + if fields: + self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) + continue + if isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) From 4f0e6cd911bf0eb71107f0a4bcbf4dec2642b5ee Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:12:59 +0530 Subject: [PATCH 218/376] fix: Sider --- erpnext/shopping_cart/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 979afd3c13d..9f06d20bde5 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() From 820a579051d26857ae52cc0a30c7aea0db79190e Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 22:38:10 +0530 Subject: [PATCH 219/376] chore: Test for Item visibility in multiple item group pages --- .../test_product_configurator.py | 63 +++++++++++++++++++ erpnext/shopping_cart/filters.py | 2 +- erpnext/shopping_cart/product_query.py | 6 +- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index 3521e7e8bf0..daaba671736 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -43,6 +43,30 @@ class TestProductConfigurator(unittest.TestCase): "show_variant_in_website": 1 }).insert() + def create_regular_web_item(self, name, item_group=None): + if not frappe.db.exists('Item', name): + doc = frappe.get_doc({ + "description": name, + "item_code": name, + "item_name": name, + "doctype": "Item", + "is_stock_item": 1, + "item_group": item_group or "_Test Item Group", + "stock_uom": "_Test UOM", + "item_defaults": [{ + "company": "_Test Company", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + "income_account": "Sales - _TC" + }], + "show_in_website": 1 + }).insert() + else: + doc = frappe.get_doc("Item", name) + return doc + def test_product_list(self): template_items = frappe.get_all('Item', {'show_in_website': 1}) variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) @@ -79,3 +103,42 @@ class TestProductConfigurator(unittest.TestCase): 'Test Size': ['2XL'] }) self.assertEqual(len(items), 1) + + def test_products_in_multiple_item_groups(self): + """Check if product is visible on multiple item group pages barring its own.""" + from erpnext.shopping_cart.product_query import ProductQuery + + if not frappe.db.exists("Item Group", {"name": "Tech Items"}): + item_group_doc = frappe.get_doc({ + "doctype": "Item Group", + "item_group_name": "Tech Items", + "parent_item_group": "All Item Groups", + "show_in_website": 1 + }).insert() + else: + item_group_doc = frappe.get_doc("Item Group", "Tech Items") + + doc = self.create_regular_web_item("Portal Item", item_group="Tech Items") + if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}): + doc.append("website_item_groups", { + "item_group": "_Test Item Group Desktops" + }) + doc.save() + + # check if item is visible in its own Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") + self.assertEqual(len(items), 1) + self.assertEqual(items[0].item_code, "Portal Item") + + # check if item is visible in configured foreign Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") + item_codes = [row.item_code for row in items] + + self.assertIn(len(items), [2, 3]) + self.assertIn("Portal Item", item_codes) + + # teardown + doc.delete() + item_group_doc.delete() \ No newline at end of file diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 9f06d20bde5..7dfa09e2d62 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname) else: doctype = df.get_link_doctype() diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index cd4a1769212..d96d803416c 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -121,12 +121,10 @@ class ProductQuery: if df.fieldtype == 'Table MultiSelect': child_doctype = df.options child_meta = frappe.get_meta(child_doctype, cached=True) - fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + fields = child_meta.get("fields") if fields: self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) - continue - - if isinstance(values, list): + elif isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) else: From ca2e9147151546390e1ff5dcd964b7842dc808c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 14:14:46 +0530 Subject: [PATCH 220/376] fix: Error while booking deferred revenue --- erpnext/accounts/deferred_revenue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index dd346bc2408..2f86c6c1de2 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): amount, base_amount = calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency) + if not amount: + return + if via_journal_entry: book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) From b2b73698745b912fd0184dba6a5440a460125a8d Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 24 Jun 2021 14:21:35 +0530 Subject: [PATCH 221/376] fix(Work Order): added freeze when trying to stop work order (#26192) * fix: added freeze when trying to stop work order * fix(ux): add freeze message --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3e5a72db9a7..8088d930df1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -704,6 +704,8 @@ erpnext.work_order = { stop_work_order: function(frm, status) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop", + freeze: true, + freeze_message: __("Updating Work Order status"), args: { work_order: frm.doc.name, status: status From 98e98d25e652bc114e2f59e58c3621887f6f7700 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 24 Jun 2021 14:24:28 +0530 Subject: [PATCH 222/376] fix(Work Order): added freeze when trying to stop work order (#26192) (#26196) * fix: added freeze when trying to stop work order * fix(ux): add freeze message Co-authored-by: Noah Jacob --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3e5a72db9a7..8088d930df1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -704,6 +704,8 @@ erpnext.work_order = { stop_work_order: function(frm, status) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop", + freeze: true, + freeze_message: __("Updating Work Order status"), args: { work_order: frm.doc.name, status: status From 7fd44907ba382ef2cb6778183a0bd7801af1b7a2 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:26:36 +0530 Subject: [PATCH 223/376] feat: fetching of qty as per received qty from PR to PI (#26184) --- .../doctype/buying_settings/buying_settings.json | 14 +++++++++++--- erpnext/controllers/accounts_controller.py | 10 ++++++++-- erpnext/patches.txt | 1 + ...ll_for_rejected_quantity_in_purchase_invoice.py | 8 ++++++++ .../doctype/purchase_receipt/purchase_receipt.py | 8 ++++++-- .../purchase_receipt/test_purchase_receipt.py | 7 +++++++ 6 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 630a1dc8cd5..b9c77d59b18 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -9,13 +9,14 @@ "supp_master_name", "supplier_group", "buying_price_list", + "maintain_same_rate_action", + "role_to_override_stop_action", "column_break_3", "po_required", "pr_required", "maintain_same_rate", - "maintain_same_rate_action", - "role_to_override_stop_action", "allow_multiple_items", + "bill_for_rejected_quantity_in_purchase_invoice", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -108,6 +109,13 @@ "fieldtype": "Link", "label": "Role Allowed to Override Stop Action", "options": "Role" + }, + { + "default": "1", + "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", + "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", + "fieldtype": "Check", + "label": "Bill for Rejected Quantity in Purchase Invoice" } ], "icon": "fa fa-cog", @@ -115,7 +123,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-04 20:01:44.087066", + "modified": "2021-06-24 10:38:28.934525", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 243939b275f..1c086e9edcd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -828,8 +828,14 @@ class AccountsController(TransactionBase): role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles(): - frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") - .format(item.item_code, item.idx, max_allowed_amt)) + if self.doctype != "Purchase Invoice": + self.throw_overbill_exception(item, max_allowed_amt) + elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")): + self.throw_overbill_exception(item, max_allowed_amt) + + def throw_overbill_exception(self, item, max_allowed_amt): + frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") + .format(item.item_code, item.idx, max_allowed_amt)) def get_company_default(self, fieldname): from erpnext.accounts.utils import get_company_default diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ed6fefdd879..dd0e33bebae 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -288,3 +288,4 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold +erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice \ No newline at end of file diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py new file mode 100644 index 00000000000..be85cfdeeff --- /dev/null +++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doctype("Buying Settings") + buying_settings = frappe.get_single("Buying Settings") + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0 + buying_settings.save() \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b8580f95a3a..e488b695b5f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -581,7 +581,6 @@ def update_billing_percentage(pr_doc, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): - from frappe.model.mapper import get_mapped_doc from erpnext.accounts.party import get_payment_terms_template doc = frappe.get_doc('Purchase Receipt', source_name) @@ -601,11 +600,16 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + target_doc.rejected_qty = 0 target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): - pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) + qty = item_row.qty + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + qty = item_row.received_qty + pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = flt(returned_qty_map.get(item_row.name, 0)) if returned_qty: if returned_qty >= pending_qty: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 95096d77d77..07c5da1dca8 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -421,11 +421,18 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(return_pr_2.items[0].qty, -3) # Make PI against unreturned amount + buying_settings = frappe.get_single("Buying Settings") + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0 + buying_settings.save() + pi = make_purchase_invoice(pr.name) pi.submit() self.assertEqual(pi.items[0].qty, 3) + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 1 + buying_settings.save() + pr.load_from_db() # PR should be completed on billing all unreturned amount self.assertEqual(pr.items[0].billed_amt, 150) From 8aa0e31bb4507a8c0451c4cd8a5305f1c37b4e93 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Jun 2021 14:29:22 +0530 Subject: [PATCH 224/376] fix: add missing semicolons --- erpnext/selling/page/point_of_sale/pos_controller.js | 5 ++--- erpnext/selling/page/point_of_sale/pos_item_details.js | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 4c938756c76..f5c5a0ae09a 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -276,14 +276,13 @@ erpnext.PointOfSale.Controller = class { form_updated: (item, field, value) => { const item_row = frappe.model.get_doc(item.doctype, item.name); if (item_row && item_row[field] != value) { - const args = { field, value, item: this.item_details.current_item - } + }; return this.on_cart_update(args) - } + }; return Promise.resolve(); }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 637fb908a8d..6a4d3d5214d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -57,7 +57,7 @@ erpnext.PointOfSale.ItemDetails = class { compare_with_current_item(item) { // returns true if `item` is currently being edited - return item && item.name == this.current_item.name + return item && item.name == this.current_item.name; } toggle_item_details_section(item) { @@ -76,7 +76,7 @@ erpnext.PointOfSale.ItemDetails = class { this.item_row = item; this.currency = this.events.get_frm().doc.currency; - this.current_item = item + this.current_item = item; this.render_dom(item); this.render_discount_dom(item); From f79a72dbf33bbc035570751dea04cf447c90bfe2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 14:14:46 +0530 Subject: [PATCH 225/376] fix: Error while booking deferred revenue --- erpnext/accounts/deferred_revenue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index dd346bc2408..2f86c6c1de2 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): amount, base_amount = calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency) + if not amount: + return + if via_journal_entry: book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) From 2fdb923953b4ed84b55771fbc07dc1556d7ac1f1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 14:56:34 +0530 Subject: [PATCH 226/376] fix(Asset Repair): Change controller hooks --- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 6054258ea69..64c51fd8c3e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -35,7 +35,7 @@ class AssetRepair(AccountsController): total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() self.total_repair_cost += total_value_of_stock_consumed - def on_submit(self): + def before_submit(self): self.check_repair_status() if self.get('stock_consumption') or self.get('capitalize_repair_cost'): @@ -52,7 +52,7 @@ class AssetRepair(AccountsController): self.asset_doc.prepare_depreciation_data() self.asset_doc.save() - def on_cancel(self): + def before_cancel(self): self.asset_doc = frappe.get_doc('Asset', self.asset) if self.get('stock_consumption') or self.get('capitalize_repair_cost'): From 4d3e748c00805ca487312006c8e967264ab6c0ef Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Jun 2021 15:01:33 +0530 Subject: [PATCH 227/376] fix: sider issues --- erpnext/selling/page/point_of_sale/pos_controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index f5c5a0ae09a..c827368dbf5 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -281,8 +281,8 @@ erpnext.PointOfSale.Controller = class { value, item: this.item_details.current_item }; - return this.on_cart_update(args) - }; + return this.on_cart_update(args); + } return Promise.resolve(); }, From 54cc1dedf2138a41fbd2d3a9247a4970fb69572c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Jun 2021 21:18:20 +0530 Subject: [PATCH 228/376] refactor(pos): use pos invoice item name as unique identifier --- .../page/point_of_sale/pos_controller.js | 123 ++++++++++-------- .../page/point_of_sale/pos_item_cart.js | 29 +---- .../page/point_of_sale/pos_item_details.js | 86 +++++------- .../page/point_of_sale/pos_item_selector.js | 6 +- 4 files changed, 113 insertions(+), 131 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ae3f9e3c9d9..4c938756c76 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -241,8 +241,8 @@ erpnext.PointOfSale.Controller = class { events: { get_frm: () => this.frm, - cart_item_clicked: (item_code, batch_no, uom, rate) => { - const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); + cart_item_clicked: (item) => { + const item_row = this.get_item_from_frm(item); this.item_details.toggle_item_details_section(item_row); }, @@ -273,17 +273,16 @@ erpnext.PointOfSale.Controller = class { this.cart.toggle_numpad(minimize); }, - form_updated: (cdt, cdn, fieldname, value) => { - const item_row = frappe.model.get_doc(cdt, cdn); - if (item_row && item_row[fieldname] != value) { + form_updated: (item, field, value) => { + const item_row = frappe.model.get_doc(item.doctype, item.name); + if (item_row && item_row[field] != value) { - const { item_code, batch_no, uom, rate } = this.item_details.current_item; - const event = { - field: fieldname, + const args = { + field, value, - item: { item_code, batch_no, uom, rate } + item: this.item_details.current_item } - return this.on_cart_update(event) + return this.on_cart_update(args) } return Promise.resolve(); @@ -300,19 +299,18 @@ erpnext.PointOfSale.Controller = class { set_value_in_current_cart_item: (selector, value) => { this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item); }, - clone_new_batch_item_in_frm: (batch_serial_map, current_item) => { + clone_new_batch_item_in_frm: (batch_serial_map, item) => { // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches // for each unique batch new item row is added in the form & cart Object.keys(batch_serial_map).forEach(batch => { - const { item_code, batch_no } = current_item; - const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no); + const item_to_clone = this.frm.doc.items.find(i => i.name == item.name); const new_row = this.frm.add_child("items", { ...item_to_clone }); // update new serialno and batch new_row.batch_no = batch; new_row.serial_no = batch_serial_map[batch].join(`\n`); new_row.qty = batch_serial_map[batch].length; this.frm.doc.items.forEach(row => { - if (item_code === row.item_code) { + if (item.item_code === row.item_code) { this.update_cart_html(row); } }); @@ -321,8 +319,8 @@ erpnext.PointOfSale.Controller = class { remove_item_from_cart: () => this.remove_item_from_cart(), get_item_stock_map: () => this.item_stock_map, close_item_details: () => { - this.item_details.toggle_item_details_section(undefined); - this.cart.prev_action = undefined; + this.item_details.toggle_item_details_section(null); + this.cart.prev_action = null; this.cart.toggle_item_highlight(); }, get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse) @@ -506,50 +504,47 @@ erpnext.PointOfSale.Controller = class { let item_row = undefined; try { let { field, value, item } = args; - const { item_code, batch_no, serial_no, uom, rate } = item; - item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); + item_row = this.get_item_from_frm(item); + const item_row_exists = !$.isEmptyObject(item_row); - const item_selected_from_selector = field === 'qty' && value === "+1" + const from_selector = field === 'qty' && value === "+1"; + if (from_selector) + value = flt(item_row.qty) + flt(value); - if (item_row) { - item_selected_from_selector && (value = item_row.qty + flt(value)) - - field === 'qty' && (value = flt(value)); + if (item_row_exists) { + if (field === 'qty') + value = flt(value); if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); } - if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) { + if (this.is_current_item_being_edited(item_row) || from_selector) { await frappe.model.set_value(item_row.doctype, item_row.name, field, value); this.update_cart_html(item_row); } } else { - if (!this.frm.doc.customer) { - frappe.dom.unfreeze(); - frappe.show_alert({ - message: __('You must select a customer before adding an item.'), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); + if (!this.frm.doc.customer) + return this.raise_customer_selection_alert(); + + const { item_code, batch_no, serial_no, rate } = item; + + if (!item_code) return; - } - if (!item_code) return; - item_selected_from_selector && (value = flt(value)) - - const args = { item_code, batch_no, rate, [field]: value }; + const new_item = { item_code, batch_no, rate, [field]: value }; if (serial_no) { await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); - args['serial_no'] = serial_no; + new_item['serial_no'] = serial_no; } - if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0; + if (field === 'serial_no') + new_item['qty'] = value.split(`\n`).length || 0; - item_row = this.frm.add_child('items', args); + item_row = this.frm.add_child('items', new_item); if (field === 'qty' && value !== 0 && !this.allow_negative_stock) await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); @@ -558,8 +553,11 @@ erpnext.PointOfSale.Controller = class { this.update_cart_html(item_row); - this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row); - this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row); + if (this.item_details.$component.is(':visible')) + this.edit_item_details_of(item_row); + + if (this.check_serial_batch_selection_needed(item_row)) + this.edit_item_details_of(item_row); } } catch (error) { @@ -570,14 +568,33 @@ erpnext.PointOfSale.Controller = class { } } - get_item_from_frm(item_code, batch_no, uom, rate) { - const has_batch_no = batch_no; - return this.frm.doc.items.find( - i => i.item_code === item_code - && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) - && (i.uom === uom) - && (i.rate == rate) - ); + raise_customer_selection_alert() { + frappe.dom.unfreeze(); + frappe.show_alert({ + message: __('You must select a customer before adding an item.'), + indicator: 'orange' + }); + frappe.utils.play_sound("error"); + } + + get_item_from_frm({ name, item_code, batch_no, uom, rate }) { + let item_row = null; + if (name) { + item_row = this.frm.doc.items.find(i => i.name == name); + } else { + // if item is clicked twice from item selector + // then "item_code, batch_no, uom, rate" will help in getting the exact item + // to increase the qty by one + const has_batch_no = batch_no; + item_row = this.frm.doc.items.find( + i => i.item_code === item_code + && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) + && (i.uom === uom) + && (i.rate == rate) + ); + } + + return item_row || {}; } edit_item_details_of(item_row) { @@ -585,9 +602,7 @@ erpnext.PointOfSale.Controller = class { } is_current_item_being_edited(item_row) { - const { item_code, batch_no } = this.item_details.current_item; - - return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true; + return item_row.name == this.item_details.current_item.name; } update_cart_html(item_row, remove_item) { @@ -669,7 +684,7 @@ erpnext.PointOfSale.Controller = class { update_item_field(value, field_or_action) { if (field_or_action === 'checkout') { - this.item_details.toggle_item_details_section(undefined); + this.item_details.toggle_item_details_section(null); } else if (field_or_action === 'remove') { this.remove_item_from_cart(); } else { @@ -688,7 +703,7 @@ erpnext.PointOfSale.Controller = class { .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); - this.item_details.toggle_item_details_section(undefined); + this.item_details.toggle_item_details_section(null); frappe.dom.unfreeze(); }) .catch(e => console.log(e)); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index f5019f5083c..9de7beff46d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -181,11 +181,8 @@ erpnext.PointOfSale.ItemCart = class { me.$totals_section.find(".edit-cart-btn").click(); } - const item_code = unescape($cart_item.attr('data-item-code')); - const batch_no = unescape($cart_item.attr('data-batch-no')); - const uom = unescape($cart_item.attr('data-uom')); - const rate = unescape($cart_item.attr('data-rate')); - me.events.cart_item_clicked(item_code, batch_no, uom, rate); + const item_row_name = unescape($cart_item.attr('data-row-name')); + me.events.cart_item_clicked({ name: item_row_name }); this.numpad_value = ''; }); @@ -521,25 +518,14 @@ erpnext.PointOfSale.ItemCart = class { } } - get_cart_item({ item_code, batch_no, uom, rate }) { - const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; - const item_code_attr = `[data-item-code="${escape(item_code)}"]`; - const uom_attr = `[data-uom="${escape(uom)}"]`; - const rate_attr = `[data-rate="${escape(rate)}"]`; - - const item_selector = batch_no ? - `.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`; - + get_cart_item({ name }) { + const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; return this.$cart_items_wrapper.find(item_selector); } get_item_from_frm(item) { const doc = this.events.get_frm().doc; - const { item_code, batch_no, uom, rate } = item; - const search_field = batch_no ? 'batch_no' : 'item_code'; - const search_value = batch_no || item_code; - - return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate); + return doc.items.find(i => i.name == item.name); } update_item_html(item, remove_item) { @@ -564,10 +550,7 @@ erpnext.PointOfSale.ItemCart = class { if (!$item_to_update.length) { this.$cart_items_wrapper.append( - `
    -
    + `
    ` ) $item_to_update = this.get_cart_item(item_data); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 5e09df8efed..43a29b9c752 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -54,36 +54,28 @@ erpnext.PointOfSale.ItemDetails = class { this.$dicount_section = this.$component.find('.discount-section'); } - has_item_has_changed(item) { - const { item_code, batch_no, uom, rate } = this.current_item; - const item_code_is_same = item && item_code === item.item_code; - const batch_is_same = item && batch_no == item.batch_no; - const uom_is_same = item && uom === item.uom; - const rate_is_same = item && rate === item.rate; - - if (!item) - return false; - - if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same) - return false; - - return true; + compare_with_current_item(item) { + // returns true if `item` is currently being edited + return item && item.name == this.current_item.name } toggle_item_details_section(item) { - this.item_has_changed = this.has_item_has_changed(item); + const current_item_changed = !this.compare_with_current_item(item); - this.events.toggle_item_selector(this.item_has_changed); - this.toggle_component(this.item_has_changed); + // if item is null or highlighted cart item is clicked twice + const hide_item_details = !Boolean(item) || !current_item_changed; + + this.events.toggle_item_selector(!hide_item_details); + this.toggle_component(!hide_item_details); - if (this.item_has_changed) { + if (item && current_item_changed) { this.doctype = item.doctype; this.item_meta = frappe.get_meta(this.doctype); this.name = item.name; this.item_row = item; this.currency = this.events.get_frm().doc.currency; - this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate }; + this.current_item = item this.render_dom(item); this.render_discount_dom(item); @@ -180,7 +172,7 @@ erpnext.PointOfSale.ItemDetails = class { df: { ...field_meta, onchange: function() { - me.events.form_updated(me.doctype, me.name, fieldname, this.value); + me.events.form_updated(me.current_item, fieldname, this.value); } }, parent: this.$form_container.find(`.${fieldname}-control`), @@ -218,22 +210,17 @@ erpnext.PointOfSale.ItemDetails = class { bind_custom_control_change_event() { const me = this; if (this.rate_control) { - if (this.allow_rate_change) { - this.rate_control.df.onchange = function() { - if (this.value || flt(this.value) === 0) { - me.events.set_value_in_current_cart_item('rate', this.value); - me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { - const item_row = frappe.get_doc(me.doctype, me.name); - const doc = me.events.get_frm().doc; - me.$item_price.html(format_currency(item_row.rate, doc.currency)); - me.render_discount_dom(item_row); - }); - me.current_item.rate = this.value; - } - }; - } else { - this.rate_control.df.read_only = 1; - } + this.rate_control.df.onchange = function() { + if (this.value || flt(this.value) === 0) { + me.events.form_updated(me.current_item, 'rate', this.value).then(() => { + const item_row = frappe.get_doc(me.doctype, me.name); + const doc = me.events.get_frm().doc; + me.$item_price.html(format_currency(item_row.rate, doc.currency)); + me.render_discount_dom(item_row); + }); + } + }; + this.rate_control.df.read_only = !this.allow_rate_change; this.rate_control.refresh(); } @@ -246,7 +233,7 @@ erpnext.PointOfSale.ItemDetails = class { this.warehouse_control.df.reqd = 1; this.warehouse_control.df.onchange = function() { if (this.value) { - me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => { + me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { me.item_stock_map = me.events.get_item_stock_map(); const available_qty = me.item_stock_map[me.item_row.item_code][this.value]; if (available_qty === undefined) { @@ -278,7 +265,7 @@ erpnext.PointOfSale.ItemDetails = class { this.serial_no_control.df.reqd = 1; this.serial_no_control.df.onchange = async function() { !me.current_item.batch_no && await me.auto_update_batch_no(); - me.events.form_updated(me.doctype, me.name, 'serial_no', this.value); + me.events.form_updated(me.current_item, 'serial_no', this.value); } this.serial_no_control.refresh(); } @@ -295,19 +282,12 @@ erpnext.PointOfSale.ItemDetails = class { } } }; - this.batch_no_control.df.onchange = function() { - me.events.set_value_in_current_cart_item('batch-no', this.value); - me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); - me.current_item.batch_no = this.value; - } this.batch_no_control.refresh(); } if (this.uom_control) { this.uom_control.df.onchange = function() { - me.events.set_value_in_current_cart_item('uom', this.value); - me.events.form_updated(me.doctype, me.name, 'uom', this.value); - me.current_item.uom = this.value; + me.events.form_updated(me.current_item, 'uom', this.value); const item_row = frappe.get_doc(me.doctype, me.name); me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value); @@ -317,9 +297,9 @@ erpnext.PointOfSale.ItemDetails = class { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { const field_control = this[`${fieldname}_control`]; - const item_is_same = !this.has_item_has_changed(item_row); + const item_row_is_being_edited = this.compare_with_current_item(item_row); - if (item_is_same && field_control && field_control.get_value() !== value) { + if (item_row_is_being_edited && field_control && field_control.get_value() !== value) { field_control.set_value(value); cur_pos.update_cart_html(item_row); } @@ -337,7 +317,9 @@ erpnext.PointOfSale.ItemDetails = class { fields: ["batch_no", "name"] }); const batch_serial_map = serials_with_batch_no.reduce((acc, r) => { - acc[r.batch_no] || (acc[r.batch_no] = []); + if (!acc[r.batch_no]) { + acc[r.batch_no] = []; + } acc[r.batch_no] = [...acc[r.batch_no], r.name]; return acc; }, {}); @@ -353,12 +335,10 @@ erpnext.PointOfSale.ItemDetails = class { if (serial_nos_belongs_to_other_batch) { this.serial_no_control.set_value(batch_serial_nos); this.qty_control.set_value(batch_serial_map[batch_no].length); - } - delete batch_serial_map[batch_no]; - - if (serial_nos_belongs_to_other_batch) + delete batch_serial_map[batch_no]; this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); + } } } diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 64c529ee4a3..dd7f143c4cb 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -232,7 +232,11 @@ erpnext.PointOfSale.ItemSelector = class { uom = uom === "undefined" ? undefined : uom; rate = rate === "undefined" ? undefined : rate; - me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }}); + me.events.item_selected({ + field: 'qty', + value: "+1", + item: { item_code, batch_no, serial_no, uom, rate } + }); me.set_search_value(''); }); From ea70f6f933b4ef6c1d1ec257244d67320e85cea7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Jun 2021 21:18:44 +0530 Subject: [PATCH 229/376] fix: hide images from cart & details --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 2 +- erpnext/selling/page/point_of_sale/pos_item_details.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 9de7beff46d..7cae0e47974 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class { function get_item_image_html() { const { image, item_name } = item_data; - if (image) { + if (!me.hide_images && image) { return `
    Date: Thu, 24 Jun 2021 14:29:22 +0530 Subject: [PATCH 230/376] fix: add missing semicolons --- erpnext/selling/page/point_of_sale/pos_controller.js | 5 ++--- erpnext/selling/page/point_of_sale/pos_item_details.js | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 4c938756c76..f5c5a0ae09a 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -276,14 +276,13 @@ erpnext.PointOfSale.Controller = class { form_updated: (item, field, value) => { const item_row = frappe.model.get_doc(item.doctype, item.name); if (item_row && item_row[field] != value) { - const args = { field, value, item: this.item_details.current_item - } + }; return this.on_cart_update(args) - } + }; return Promise.resolve(); }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 637fb908a8d..6a4d3d5214d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -57,7 +57,7 @@ erpnext.PointOfSale.ItemDetails = class { compare_with_current_item(item) { // returns true if `item` is currently being edited - return item && item.name == this.current_item.name + return item && item.name == this.current_item.name; } toggle_item_details_section(item) { @@ -76,7 +76,7 @@ erpnext.PointOfSale.ItemDetails = class { this.item_row = item; this.currency = this.events.get_frm().doc.currency; - this.current_item = item + this.current_item = item; this.render_dom(item); this.render_discount_dom(item); From 3b126014613d2ecdc97493b47985bbd628e78451 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Jun 2021 15:01:33 +0530 Subject: [PATCH 231/376] fix: sider issues --- erpnext/selling/page/point_of_sale/pos_controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index f5c5a0ae09a..c827368dbf5 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -281,8 +281,8 @@ erpnext.PointOfSale.Controller = class { value, item: this.item_details.current_item }; - return this.on_cart_update(args) - }; + return this.on_cart_update(args); + } return Promise.resolve(); }, From e21d44c5c3b0ba8ef2252aba8fb2714e078bb159 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 15:04:44 +0530 Subject: [PATCH 232/376] fix(Asset): Remove to_date field --- erpnext/assets/doctype/asset/asset.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index d77eb104184..de060757e2e 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -53,7 +53,6 @@ "next_depreciation_date", "section_break_14", "schedules", - "to_date", "insurance_details", "policy_number", "insurer", @@ -481,12 +480,6 @@ "fieldname": "section_break_36", "fieldtype": "Section Break", "label": "Finance Books" - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 1, - "label": "To Date" } ], "idx": 72, @@ -509,7 +502,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-06-19 13:56:58.450182", + "modified": "2021-06-24 14:58:51.097908", "modified_by": "Administrator", "module": "Assets", "name": "Asset", From b5d1a7731cf2c7eb1e77475a4cbd3fdda5c5db47 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 24 Jun 2021 15:55:50 +0550 Subject: [PATCH 233/376] bumped to version 13.5.2 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 60c614f6f59..39d9a27615e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.5.1' +__version__ = '13.5.2' def get_default_company(user=None): '''Get default company for user''' From dbdf2515cd92669ed2ed0c6b71302d5ee6ad89a3 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 23 Jun 2021 09:54:12 +0530 Subject: [PATCH 234/376] fix: fetches correct preferred shipping address --- erpnext/accounts/custom/address.py | 2 ++ erpnext/public/js/utils/party.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index 5e764037a74..c417a493c69 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -33,6 +33,8 @@ def get_shipping_address(company, address = None): if address and frappe.db.get_value('Dynamic Link', {'parent': address, 'link_name': company}): filters.append(["Address", "name", "=", address]) + if not address: + filters.append(["Address", "is_shipping_address", "=", 1]) address = frappe.get_all("Address", filters=filters, fields=fields) or {} diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 808dd5add05..99c8587391c 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -274,9 +274,9 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { return true; } -erpnext.utils.get_shipping_address = function(frm, callback){ +erpnext.utils.get_shipping_address = function(frm, callback) { if (frm.doc.company) { - if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + if ((frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || frm.doc.internal_order_reference)) { if (callback) { return callback(); From da82bd4b51ff2670a5041bef3e28005adb39d2df Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 24 Jun 2021 17:23:21 +0530 Subject: [PATCH 235/376] refactor: update cost updates operation time and hour rates in BOM (#25891) * refactor: updates hour_rate and operation time on update cost * refactor: hour_rates are updated in routing when updated in workstations * test: test cases for updating hour_rates and operation time in linked bom --- erpnext/manufacturing/doctype/bom/bom.py | 45 +++++++++----- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../manufacturing/doctype/routing/routing.py | 14 ++++- .../doctype/routing/test_routing.py | 58 +++++++++++++++-- .../doctype/workstation/test_workstation.py | 62 ++++++++++++++++++- .../doctype/workstation/workstation.py | 5 +- 6 files changed, 157 insertions(+), 29 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d1f63854c71..3f109d91b5e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -81,7 +81,7 @@ class BOM(WebsiteGenerator): self.validate_operations() self.calculate_cost() self.update_stock_qty() - self.update_cost(update_parent=False, from_child_bom=True, save=False) + self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -213,7 +213,7 @@ class BOM(WebsiteGenerator): return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) @frappe.whitelist() - def update_cost(self, update_parent=True, from_child_bom=False, save=True): + def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True): if self.docstatus == 2: return @@ -242,7 +242,7 @@ class BOM(WebsiteGenerator): if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True - self.calculate_cost() + self.calculate_cost(update_hour_rate) if save: self.db_update() @@ -403,32 +403,47 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self): + def calculate_cost(self, update_hour_rate = False): """Calculate bom totals""" - self.calculate_op_cost() + self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost() self.calculate_sm_cost() self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost - def calculate_op_cost(self): + def calculate_op_cost(self, update_hour_rate = False): """Update workstation rate and calculates totals""" self.operating_cost = 0 self.base_operating_cost = 0 for d in self.get('operations'): if d.workstation: - if not d.hour_rate: - hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate")) - d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate - - if d.hour_rate and d.time_in_mins: - d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate) - d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 - d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate) + self.update_rate_and_time(d, update_hour_rate) self.operating_cost += flt(d.operating_cost) self.base_operating_cost += flt(d.base_operating_cost) + def update_rate_and_time(self, row, update_hour_rate = False): + if not row.hour_rate or update_hour_rate: + hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) + row.hour_rate = (hour_rate / flt(self.conversion_rate) + if self.conversion_rate and hour_rate else hour_rate) + + if self.routing: + row.time_in_mins = flt(frappe.db.get_value("BOM Operation", { + "workstation": row.workstation, + "operation": row.operation, + "sequence_id": row.sequence_id, + "parent": self.routing + }, ["time_in_mins"])) + + if row.hour_rate and row.time_in_mins: + row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + + if update_hour_rate: + row.db_update() + def calculate_rm_cost(self): """Fetch RM rate as per today's valuation rate and calculate totals""" total_rm_cost = 0 @@ -975,7 +990,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - + return frappe.get_all("Item", fields = fields, filters=query_filters, or_filters = or_cond_filters, order_by=order_by, diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 42b23f223d2..1f443fb95ae 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -123,7 +123,7 @@ class TestBOM(unittest.TestCase): bom.items[0].conversion_factor = 5 bom.insert() - bom.update_cost() + bom.update_cost(update_hour_rate = False) # test amounts in selected currency self.assertEqual(bom.items[0].rate, 300) diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index 8312d7436c2..ece0db717a2 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -4,14 +4,24 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint +from frappe.utils import cint, flt from frappe import _ from frappe.model.document import Document class Routing(Document): def validate(self): + self.calculate_operating_cost() self.set_routing_id() + def on_update(self): + self.calculate_operating_cost() + + def calculate_operating_cost(self): + for operation in self.operations: + if not operation.hour_rate: + operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') + operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2) + def set_routing_id(self): sequence_id = 0 for row in self.operations: @@ -21,4 +31,4 @@ class Routing(Document): frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") .format(row.idx, row.sequence_id, sequence_id)) - sequence_id = row.sequence_id \ No newline at end of file + sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 6a38dcfa030..92f26946ab7 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -7,9 +7,7 @@ import unittest import frappe from frappe.test_runner import make_test_records from erpnext.stock.doctype.item.test_item import make_item -from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError -from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record class TestRouting(unittest.TestCase): @@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase): wo_doc.cancel() wo_doc.delete() + def test_update_bom_operation_time(self): + operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "hour_rate_labour": 750 , + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation B", + "hour_rate_labour": 200, + "hour_rate_rent": 1000, + "time_in_mins": 20 + } + ] + + test_routing_operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "time_in_mins": 20 + } + ] + setup_operations(operations) + routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR') + self.assertEqual(routing_doc.operations[0].time_in_mins, 30) + self.assertEqual(routing_doc.operations[1].time_in_mins, 20) + routing_doc.operations[0].time_in_mins = 90 + routing_doc.operations[1].time_in_mins = 42.2 + routing_doc.save() + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(bom_doc.operations[0].time_in_mins, 90) + self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2) + + def setup_operations(rows): + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + from erpnext.manufacturing.doctype.operation.test_operation import make_operation for row in rows: make_workstation(row) make_operation(row) @@ -61,12 +105,14 @@ def create_routing(**args): if not args.do_not_save: try: - for operation in args.operations: - doc.append("operations", operation) - doc.insert() except frappe.DuplicateEntryError: doc = frappe.get_doc("Routing", args.routing_name) + doc.delete_key('operations') + for operation in args.operations: + doc.append("operations", operation) + + doc.save() return doc @@ -91,7 +137,7 @@ def setup_bom(**args): name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name') if not name: bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"), - routing = args.routing, with_operations=1) + routing = args.routing, with_operations=1, currency = args.currency) else: bom_doc = frappe.get_doc("BOM", name) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index c6699bee48c..9b73aca6010 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -1,16 +1,19 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt from __future__ import unicode_literals +from erpnext.manufacturing.doctype.operation.test_operation import make_operation import frappe import unittest from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError +from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing +from frappe.test_runner import make_test_records test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') +make_test_records('Workstation') class TestWorkstation(unittest.TestCase): - def test_validate_timings(self): check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") @@ -21,6 +24,58 @@ class TestWorkstation(unittest.TestCase): self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + def test_update_bom_operation_rate(self): + operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "time_in_mins": 60 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation B", + "hour_rate_rent": 1000, + "time_in_mins": 60 + } + ] + + for row in operations: + make_workstation(row) + make_operation(row) + + test_routing_operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "time_in_mins": 60 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "time_in_mins": 60 + } + ] + routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations) + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") + w1 = frappe.get_doc("Workstation", "_Test Workstation A") + #resets values + w1.hour_rate_rent = 300 + w1.hour_rate_labour = 0 + w1.save() + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(w1.hour_rate, 300) + self.assertEqual(bom_doc.operations[0].hour_rate, 300) + w1.hour_rate_rent = 250 + w1.save() + #updating after setting new rates in workstations + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(w1.hour_rate, 250) + self.assertEqual(bom_doc.operations[0].hour_rate, 250) + self.assertEqual(bom_doc.operations[1].hour_rate, 250) + def make_workstation(*args, **kwargs): args = args if args else kwargs if isinstance(args, tuple): @@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs): "doctype": "Workstation", "workstation_name": workstation_name }) - + doc.hour_rate_rent = args.get("hour_rate_rent") + doc.hour_rate_labour = args.get("hour_rate_labour") doc.insert() return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Workstation", workstation_name) \ No newline at end of file + return frappe.get_doc("Workstation", workstation_name) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 3512e590458..f4483f75472 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -39,7 +39,8 @@ class Workstation(Document): def update_bom_operation(self): bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` - where workstation = %s""", self.name) + where workstation = %s and parenttype = 'routing' """, self.name) + for bom_no in bom_list: frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s where parent = %s and workstation = %s""", @@ -71,7 +72,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da def is_within_operating_hours(workstation, operation, from_datetime, to_datetime): operation_length = time_diff_in_seconds(to_datetime, from_datetime) workstation = frappe.get_doc("Workstation", workstation) - + if not workstation.working_hours: return From f84f8d52602bf529a96b06afa24cf4eaf71ea280 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 24 Jun 2021 17:29:55 +0530 Subject: [PATCH 236/376] Update party.js --- erpnext/public/js/utils/party.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 99c8587391c..a79eadc7619 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -276,7 +276,7 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { erpnext.utils.get_shipping_address = function(frm, callback) { if (frm.doc.company) { - if ((frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference || frm.doc.internal_order_reference)) { if (callback) { return callback(); From 755ebdf5828f31c0f8558bcceb20b4b1605586d7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 24 Jun 2021 17:35:14 +0530 Subject: [PATCH 237/376] Update party.js --- erpnext/public/js/utils/party.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 99c8587391c..a79eadc7619 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -276,7 +276,7 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { erpnext.utils.get_shipping_address = function(frm, callback) { if (frm.doc.company) { - if ((frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference || frm.doc.internal_order_reference)) { if (callback) { return callback(); From 9dc625c1c9b841d950829e1b33024e596b742b45 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 24 Jun 2021 17:36:39 +0530 Subject: [PATCH 238/376] fix: validate product bundle for existing transactions before deletion (#25978) --- .../doctype/product_bundle/product_bundle.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index d3281f733f0..ae3482f402b 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -4,6 +4,8 @@ from __future__ import unicode_literals import frappe +from frappe.utils import get_link_to_form + from frappe import _ from frappe.model.document import Document @@ -18,6 +20,27 @@ class ProductBundle(Document): from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "uom", "qty") + def on_trash(self): + linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice", + "Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"] + + invoice_links = [] + for doctype in linked_doctypes: + item_doctype = doctype + " Item" + + if doctype == "Stock Entry": + item_doctype = doctype + " Detail" + + invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"]) + + for invoice in invoices: + invoice_links.append(get_link_to_form(doctype, invoice['parent'])) + + if len(invoice_links): + frappe.throw( + "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" + .format(", ".join(invoice_links)), title=_("Not Allowed")) + def validate_main_item(self): """Validates, main Item is not a stock item""" if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"): From 1ca8f6a51d6ac6a374af3cf95a23b51d3e33f3ea Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Jun 2021 13:05:43 +0530 Subject: [PATCH 239/376] fix: purchase receipt gl entries with same item code --- erpnext/accounts/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 59009ae6215..25d2cf10bd4 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None): def check_if_in_list(gle, gl_map, dimensions=None): account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type', - 'cost_center', 'project'] + 'cost_center', 'project', 'voucher_detail_no'] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions From f6dce4df73b2a61ca93e8c4119b2a2ce3ead39b6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Jun 2021 11:18:56 +0530 Subject: [PATCH 240/376] test: service item purchase with perpetual inventory enabled --- .../purchase_receipt/test_purchase_receipt.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 07c5da1dca8..2eb8bfd5d2f 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1011,6 +1011,47 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) + def test_service_item_purchase_with_perpetual_inventory(self): + company = '_Test Company with perpetual inventory' + service_item = '_Test Non Stock Item' + + before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items') + frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1) + srbnb_account = 'Stock Received But Not Billed - TCP1' + frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account) + + pr = make_purchase_receipt( + company=company, item=service_item, + warehouse='Finished Goods - TCP1', do_not_save=1 + ) + item_row_with_diff_rate = frappe.copy_doc(pr.items[0]) + item_row_with_diff_rate.rate = 100 + pr.append('items', item_row_with_diff_rate) + + pr.save() + pr.submit() + + item_one_gl_entry = frappe.db.get_all("GL Entry", { + 'voucher_type': pr.doctype, + 'voucher_no': pr.name, + 'account': srbnb_account, + 'voucher_detail_no': pr.items[0].name + }, pluck="name") + + item_two_gl_entry = frappe.db.get_all("GL Entry", { + 'voucher_type': pr.doctype, + 'voucher_no': pr.name, + 'account': srbnb_account, + 'voucher_detail_no': pr.items[1].name + }, pluck="name") + + # check if the entries are not merged into one + # seperate entries should be made since voucher_detail_no is different + self.assertEqual(len(item_one_gl_entry), 1) + self.assertEqual(len(item_two_gl_entry), 1) + + frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 6e8148909540f358f4cffc00dce29e3e70cf671d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 5 Jan 2021 14:18:26 +0530 Subject: [PATCH 241/376] feat: Job Card Enhancements --- .../doctype/bom_operation/bom_operation.json | 9 +- .../doctype/job_card/job_card.js | 136 +++++---- .../doctype/job_card/job_card.json | 64 ++-- .../doctype/job_card/job_card.py | 95 +++++- .../doctype/job_card_operation/__init__.py | 0 .../job_card_operation.json | 52 ++++ .../job_card_operation/job_card_operation.py | 10 + .../job_card_time_log/job_card_time_log.json | 24 +- .../manufacturing_settings.json | 19 +- .../doctype/operation/operation.js | 4 +- .../doctype/operation/operation.json | 274 ++++++++---------- .../doctype/operation/operation.py | 26 ++ .../doctype/sub_operation/__init__.py | 0 .../doctype/sub_operation/sub_operation.js | 8 + .../doctype/sub_operation/sub_operation.json | 51 ++++ .../doctype/sub_operation/sub_operation.py | 10 + .../sub_operation/test_sub_operation.py | 10 + .../doctype/work_order/work_order.js | 3 +- .../doctype/work_order/work_order.json | 55 ++++ .../doctype/work_order/work_order.py | 134 +++++++-- .../doctype/work_order_batch/__init__.py | 0 .../work_order_batch/work_order_batch.json | 49 ++++ .../work_order_batch/work_order_batch.py | 10 + erpnext/stock/doctype/batch/batch.py | 9 +- .../stock/doctype/stock_entry/stock_entry.py | 81 +++++- 25 files changed, 834 insertions(+), 299 deletions(-) create mode 100644 erpnext/manufacturing/doctype/job_card_operation/__init__.py create mode 100644 erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json create mode 100644 erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py create mode 100644 erpnext/manufacturing/doctype/sub_operation/__init__.py create mode 100644 erpnext/manufacturing/doctype/sub_operation/sub_operation.js create mode 100644 erpnext/manufacturing/doctype/sub_operation/sub_operation.json create mode 100644 erpnext/manufacturing/doctype/sub_operation/sub_operation.py create mode 100644 erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py create mode 100644 erpnext/manufacturing/doctype/work_order_batch/__init__.py create mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json create mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 07464e3e766..57062b8ca4c 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -13,10 +13,10 @@ "col_break1", "hour_rate", "time_in_mins", - "batch_size", "operating_cost", "base_hour_rate", "base_operating_cost", + "batch_size", "image" ], "fields": [ @@ -61,6 +61,8 @@ }, { "description": "In minutes", + "fetch_from": "operation.total_operation_time", + "fetch_if_empty": 1, "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 1, @@ -104,7 +106,8 @@ "label": "Image" }, { - "default": "1", + "fetch_from": "operation.batch_size", + "fetch_if_empty": 1, "fieldname": "batch_size", "fieldtype": "Int", "label": "Batch Size" @@ -120,7 +123,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-13 18:14:10.018774", + "modified": "2020-12-14 15:01:33.142869", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 4e8dd41022b..57ec20b42c9 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', { } }; }); + + frm.set_indicator_formatter('sub_operation', + function(doc) { + if (doc.status == "Pending") { + return "red"; + } else { + return doc.status === "Complete" ? "green" : "orange"; + } + } + ); }, refresh: function(frm) { @@ -97,81 +107,76 @@ frappe.ui.form.on('Job Card', { prepare_timer_buttons: function(frm) { frm.trigger("make_dashboard"); - if (!frm.doc.job_started) { - frm.add_custom_button(__("Start"), () => { - if (!frm.doc.employee) { - frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", - fieldname: 'employee'}, d => { - if (d.employee) { - frm.set_value("employee", d.employee); - } else { - frm.events.start_job(frm); - } - }, __("Enter Value"), __("Start")); - } else { - frm.events.start_job(frm); - } + + if (!frm.doc.started_time && !frm.doc.current_time) { + frm.add_custom_button(__("Start Job"), () => { + frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Employee'), options: "Job Card Time Log", + fieldname: 'employee'}, d => { + debugger + frm.events.start_job(frm, "Work In Progress", d.employee); + }, __("Assign Job to Employee")); }).addClass("btn-primary"); } else if (frm.doc.status == "On Hold") { - frm.add_custom_button(__("Resume"), () => { - frappe.flags.resume_job = 1; - frm.events.start_job(frm); + frm.add_custom_button(__("Resume Job"), () => { + frm.events.start_job(frm, "Resume Job"); }).addClass("btn-primary"); } else { - frm.add_custom_button(__("Pause"), () => { - frappe.flags.pause_job = 1; - frm.set_value("status", "On Hold"); - frm.events.complete_job(frm); + frm.add_custom_button(__("Pause Job"), () => { + frm.events.complete_job(frm, "On Hold"); }); - frm.add_custom_button(__("Complete"), () => { - let completed_time = frappe.datetime.now_datetime(); - frm.trigger("hide_timer"); - - if (frm.doc.for_quantity) { - frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), - fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { - frm.events.complete_job(frm, completed_time, data.qty); - }, __("Enter Value"), __("Complete")); - } else { - frm.events.complete_job(frm, completed_time, 0); - } + frm.add_custom_button(__("Complete Job"), () => { + frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), + fieldname: 'qty', default: frm.doc.for_quantity}, data => { + frm.events.complete_job(frm, "Complete", data.qty); + }, __("Enter Value")); }).addClass("btn-primary"); } }, - start_job: function(frm) { - let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); - row.from_time = frappe.datetime.now_datetime(); - frm.set_value('job_started', 1); - frm.set_value('started_time' , row.from_time); - frm.set_value("status", "Work In Progress"); - - if (!frappe.flags.resume_job) { - frm.set_value('current_time' , 0); - } - - frm.save(); + start_job: function(frm, status, employee) { + const args = { + job_card_id: frm.doc.name, + start_time: frappe.datetime.now_datetime(), + employee: employee, + status: status + }; + frm.events.make_time_log(frm, args); }, - complete_job: function(frm, completed_time, completed_qty) { - frm.doc.time_logs.forEach(d => { - if (d.from_time && !d.to_time) { - d.to_time = completed_time || frappe.datetime.now_datetime(); - d.completed_qty = completed_qty || 0; + complete_job: function(frm, status, completed_qty) { + const args = { + job_card_id: frm.doc.name, + complete_time: frappe.datetime.now_datetime(), + status: status, + completed_qty: completed_qty + }; + frm.events.make_time_log(frm, args); + }, - if(frappe.flags.pause_job) { - let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; - frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0)); - } else { - frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.set_value('current_time' , 0); - } + make_time_log: function(frm, args) { + frm.events.update_sub_operation(frm, args); - frm.save(); + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log", + args: { + args: args + }, + freeze: true, + callback: function (r) { + frm.reload_doc(); + frm.trigger("make_dashboard"); } - }); + }) + }, + + update_sub_operation: function(frm, args) { + if (frm.doc.sub_operations && frm.doc.sub_operations.length) { + let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete'); + if (sub_operations && sub_operations.length) { + args["sub_operation"] = sub_operations[0].sub_operation; + } + } }, validate: function(frm) { @@ -180,18 +185,8 @@ frappe.ui.form.on('Job Card', { } }, - employee: function(frm) { - if (frm.doc.job_started && !frm.doc.current_time) { - frm.trigger("reset_timer"); - } else { - frm.events.start_job(frm); - } - }, - reset_timer: function(frm) { frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.set_value('current_time' , 0); }, make_dashboard: function(frm) { @@ -297,7 +292,6 @@ frappe.ui.form.on('Job Card Time Log', { }, to_time: function(frm) { - frm.set_value('job_started', 0); frm.set_value('started_time', ''); } }) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 5713f697e99..c2fd8cc3f92 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -9,30 +9,30 @@ "naming_series", "work_order", "bom_no", - "workstation", - "operation", - "operation_row_number", "column_break_4", "posting_date", "company", - "remarks", "production_section", "production_item", "item_name", "for_quantity", - "quality_inspection", - "wip_warehouse", "column_break_12", - "employee", - "employee_name", - "status", + "wip_warehouse", + "quality_inspection", "project", + "operation_section_section", + "operation", + "operation_row_number", + "column_break_18", + "workstation", + "section_break_21", + "sub_operations", "timing_detail", "time_logs", "section_break_13", "total_completed_qty", - "total_time_in_mins", "column_break_15", + "total_time_in_mins", "section_break_8", "items", "more_information", @@ -40,7 +40,9 @@ "sequence_id", "transferred_qty", "requested_qty", + "status", "column_break_20", + "remarks", "barcode", "job_started", "started_time", @@ -117,13 +119,6 @@ "fieldtype": "Section Break", "label": "Timing Detail" }, - { - "fieldname": "employee", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "Employee", - "options": "Employee" - }, { "allow_bulk_edit": 1, "fieldname": "time_logs", @@ -133,9 +128,11 @@ }, { "fieldname": "section_break_13", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { + "default": "0", "fieldname": "total_completed_qty", "fieldtype": "Float", "label": "Total Completed Qty", @@ -251,12 +248,7 @@ "reqd": 1 }, { - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Read Only", - "label": "Employee Name" - }, - { + "collapsible": 1, "fieldname": "production_section", "fieldtype": "Section Break", "label": "Production" @@ -314,11 +306,33 @@ "label": "Quality Inspection", "no_copy": 1, "options": "Quality Inspection" + }, + { + "allow_bulk_edit": 1, + "fieldname": "sub_operations", + "fieldtype": "Table", + "label": "Sub Operations", + "options": "Job Card Operation", + "read_only": 1 + }, + { + "fieldname": "operation_section_section", + "fieldtype": "Section Break", + "label": "Operation Section" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_21", + "fieldtype": "Section Break", + "hide_border": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-11-19 18:26:50.531664", + "modified": "2020-12-14 15:14:05.566271", "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 cdc45188942..5c157d43ec8 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -4,12 +4,13 @@ from __future__ import unicode_literals import frappe -import datetime +import datetime, json from frappe import _, bold +from six import string_types from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, - get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form) + get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds) from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations @@ -25,9 +26,20 @@ class JobCard(Document): self.set_status() self.validate_operation_id() self.validate_sequence_id() + self.get_sub_operations() + self.update_sub_operation_status() + + def get_sub_operations(self): + if self.operation: + self.sub_operations = [] + for row in frappe.get_all("Sub Operation", + filters = {"parent": self.operation}, fields=["operation"]): + self.append("sub_operations", { + "sub_operation": row.operation, + "status": "Pending" + }) def validate_time_logs(self): - self.total_completed_qty = 0.0 self.total_time_in_mins = 0.0 if self.get('time_logs'): @@ -46,6 +58,8 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty + else: + self.total_completed_qty = 0.0 self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) @@ -57,7 +71,7 @@ class JobCard(Document): self.workstation, 'production_capacity') or 1 validate_overlap_for = " and jc.workstation = %(workstation)s " - if self.employee: + if args.get("employee"): # override capacity for employee production_capacity = 1 validate_overlap_for = " and jc.employee = %(employee)s " @@ -80,7 +94,7 @@ class JobCard(Document): "to_time": args.to_time, "name": args.name or "No Name", "parent": args.parent or "No Name", - "employee": self.employee, + "employee": args.get("employee"), "workstation": self.workstation }, as_dict=True) @@ -158,6 +172,66 @@ class JobCard(Document): row.planned_start_time = datetime.datetime.combine(start_date, get_time(workstation_doc.working_hours[0].start_time)) + def add_time_log(self, args): + last_row = [] + if self.time_logs and len(self.time_logs) > 0: + last_row = self.time_logs[-1] + + self.reset_timer_value(args) + if last_row and args.get("complete_time"): + last_row.update({ + "to_time": get_datetime(args.get("complete_time")), + "operation": args.get("sub_operation"), + "completed_qty": args.get("completed_qty") or 0.0 + }) + elif args.get("start_time"): + self.append("time_logs", { + "from_time": get_datetime(args.get("start_time")), + "employee": args.get("employee"), + "operation": args.get("sub_operation"), + "completed_qty": 0.0 + }) + + if self.status == "On Hold": + self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) + + self.save() + + def reset_timer_value(self, args): + self.started_time = None + + if args.get("status") in ["Work In Progress", "Complete"]: + self.current_time = 0.0 + + if args.get("status") == "Work In Progress": + self.started_time = get_datetime(args.get("start_time")) + + if args.get("status") == "Resume Job": + args["status"] = "Work In Progress" + + if args.get("status"): + self.status = args.get("status") + + def update_sub_operation_status(self): + if not (self.sub_operations and self.time_logs): return + + operation_wise_completed_time = {} + for time_log in self.time_logs: + if time_log.operation not in operation_wise_completed_time: + operation_wise_completed_time.setdefault(time_log.operation, + frappe._dict({"status": "Pending", "completed_time": 0.0})) + + op_row = operation_wise_completed_time[time_log.operation] + op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" + if time_log.time_in_mins: + op_row.completed_time += time_log.time_in_mins + + for row in self.sub_operations: + operation_deatils = operation_wise_completed_time.get(row.sub_operation) + if operation_deatils: + row.status = operation_deatils.status + row.completed_time = operation_deatils.completed_time + def update_time_logs(self, row): self.append("time_logs", { "from_time": row.planned_start_time, @@ -376,6 +450,17 @@ class JobCard(Document): frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) + +@frappe.whitelist() +def make_time_log(args): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + doc = frappe.get_doc("Job Card", args.job_card_id) + doc.validate_sequence_id() + doc.add_time_log(args) + @frappe.whitelist() def get_operation_details(work_order, operation): if work_order and operation: diff --git a/erpnext/manufacturing/doctype/job_card_operation/__init__.py b/erpnext/manufacturing/doctype/job_card_operation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json new file mode 100644 index 00000000000..be8190236d1 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "creation": "2020-12-07 16:58:38.449041", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sub_operation", + "completed_time", + "status" + ], + "fields": [ + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Complete\nPause\nPending\nWork In Progress", + "read_only": 1 + }, + { + "description": "In mins", + "fieldname": "completed_time", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Completed Time", + "read_only": 1 + }, + { + "fieldname": "sub_operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-14 17:08:25.992957", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Operation", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py new file mode 100644 index 00000000000..85d72982ed3 --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class JobCardOperation(Document): + pass diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json index 9dd54dd6182..a7102d7d237 100644 --- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json +++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json @@ -1,14 +1,17 @@ { + "actions": [], "creation": "2019-03-08 23:56:43.187569", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "employee", "from_time", "to_time", "column_break_2", "time_in_mins", - "completed_qty" + "completed_qty", + "operation" ], "fields": [ { @@ -41,10 +44,27 @@ "in_list_view": 1, "label": "Completed Qty", "reqd": 1 + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee" + }, + { + "fieldname": "operation", + "fieldtype": "Link", + "label": "Operation", + "no_copy": 1, + "options": "Operation", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-12-03 12:56:02.285448", + "links": [], + "modified": "2020-12-23 14:30:00.970916", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Time Log", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index b7634da87c2..6647be54ebd 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -26,7 +26,9 @@ "column_break_16", "overproduction_percentage_for_work_order", "other_settings_section", - "update_bom_costs_automatically" + "update_bom_costs_automatically", + "column_break_23", + "make_serial_no_batch_from_work_order" ], "fields": [ { @@ -155,13 +157,24 @@ { "fieldname": "column_break_5", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order", + "fieldname": "make_serial_no_batch_from_work_order", + "fieldtype": "Check", + "label": "Make Serial No / Batch from Work Order" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 10:55:43.996581", + "modified": "2020-12-08 13:37:40.325838", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", @@ -178,4 +191,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js index 5c2aba6f095..9bfcc6eedb6 100644 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ b/erpnext/manufacturing/doctype/operation/operation.js @@ -2,7 +2,5 @@ // For license information, please see license.txt frappe.ui.form.on('Operation', { - refresh: function(frm) { - } -}); +}); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json index c231fba2faa..9e6f8e1f5d7 100644 --- a/erpnext/manufacturing/doctype/operation/operation.json +++ b/erpnext/manufacturing/doctype/operation/operation.json @@ -1,167 +1,133 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2014-11-07 16:20:30.683186", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2014-11-07 16:20:30.683186", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "workstation", + "data_2", + "cost_of_poor_quality_operation", + "job_card_section", + "create_job_card_based_on_batch_size", + "column_break_6", + "batch_size", + "sub_operations_section", + "sub_operations", + "total_operation_time", + "section_break_4", + "description" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "workstation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Default Workstation", - "length": 0, - "no_copy": 0, - "options": "Workstation", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Default Workstation", + "options": "Workstation" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Operation Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + }, + { + "collapsible": 1, + "fieldname": "sub_operations_section", + "fieldtype": "Section Break", + "label": "Sub Operations" + }, + { + "fieldname": "sub_operations", + "fieldtype": "Table", + "options": "Sub Operation" + }, + { + "description": "Time in mins.", + "fieldname": "total_operation_time", + "fieldtype": "Float", + "label": "Total Operation Time", + "read_only": 1 + }, + { + "fieldname": "data_2", + "fieldtype": "Column Break" + }, + { + "default": "1", + "depends_on": "create_job_card_based_on_batch_size", + "fieldname": "batch_size", + "fieldtype": "Int", + "label": "Batch Size", + "mandatory_depends_on": "create_job_card_based_on_batch_size" + }, + { + "default": "0", + "fieldname": "create_job_card_based_on_batch_size", + "fieldtype": "Check", + "label": "Create Job Card based on Batch Size" + }, + { + "default": "0", + "description": "Cost of poor quality operation", + "fieldname": "cost_of_poor_quality_operation", + "fieldtype": "Check", + "label": "Is COPQ Operation" + }, + { + "collapsible": 1, + "fieldname": "job_card_section", + "fieldtype": "Section Break", + "label": "Job Card" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-wrench", - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:28:27.462413", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Operation", - "name_case": "", - "owner": "Administrator", + ], + "icon": "fa fa-wrench", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-24 14:25:03.428303", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Operation", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "read": 1, + "role": "Manufacturing User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "Manufacturing Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py index 69e83292ff8..aaf0d5c01b4 100644 --- a/erpnext/manufacturing/doctype/operation/operation.py +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -2,9 +2,35 @@ # For license information, please see license.txt from __future__ import unicode_literals + +import frappe +from frappe import _ +from frappe.utils import flt from frappe.model.document import Document class Operation(Document): def validate(self): if not self.description: self.description = self.name + + self.duplicate_sub_operation() + self.set_total_time() + + def duplicate_sub_operation(self): + operation_list = [] + for row in self.sub_operations: + if row.operation in operation_list: + frappe.throw(_("The operation {0} can not add multiple times") + .format(frappe.bold(row.operation))) + + if self.name == row.operation: + frappe.throw(_("The operation {0} can not be the sub operation") + .format(frappe.bold(row.operation))) + + operation_list.append(row.operation) + + def set_total_time(self): + self.total_operation_time = 0.0 + + for row in self.sub_operations: + self.total_operation_time += row.time_in_mins diff --git a/erpnext/manufacturing/doctype/sub_operation/__init__.py b/erpnext/manufacturing/doctype/sub_operation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js new file mode 100644 index 00000000000..be9db6a4089 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Sub Operation', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json new file mode 100644 index 00000000000..f63d2b98641 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "creation": "2020-12-07 15:39:47.488519", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "operation", + "time_in_mins", + "column_break_5", + "description" + ], + "fields": [ + { + "fieldname": "operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation" + }, + { + "description": "Time in mins", + "fieldname": "time_in_mins", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Operation Time" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-07 18:09:18.005578", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Sub Operation", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py new file mode 100644 index 00000000000..f4b27758e9d --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class SubOperation(Document): + pass diff --git a/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py new file mode 100644 index 00000000000..d3410ca3120 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSubOperation(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 8088d930df1..601734914da 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", { } if (frm.doc.docstatus === 1 - && frm.doc.operations && frm.doc.operations.length - && frm.doc.qty != frm.doc.material_transferred_for_manufacturing) { + && frm.doc.operations && frm.doc.operations.length) { const not_completed = frm.doc.operations.filter(d => { if(d.status != 'Completed') { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index cd9edeeea83..cb3c9421077 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -21,6 +21,13 @@ "produced_qty", "sales_order", "project", + "serial_no_and_batch_for_finished_good_section", + "has_serial_no", + "has_batch_no", + "column_break_17", + "serial_no", + "batch_size", + "batches", "settings_section", "allow_alternative_item", "use_multi_level_bom", @@ -488,6 +495,54 @@ "fieldtype": "Float", "label": "Lead Time", "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:!doc.__islocal", + "fieldname": "serial_no_and_batch_for_finished_good_section", + "fieldtype": "Section Break", + "label": "Serial No and Batch for Finished Good" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "production_item.has_serial_no", + "fieldname": "has_serial_no", + "fieldtype": "Check", + "label": "Has Serial No", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "production_item.has_batch_no", + "fieldname": "has_batch_no", + "fieldtype": "Check", + "label": "Has Batch No", + "read_only": 1 + }, + { + "depends_on": "has_serial_no", + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial Nos" + }, + { + "default": "0", + "depends_on": "has_batch_no", + "fieldname": "batch_size", + "fieldtype": "Float", + "label": "Batch Size" + }, + { + "depends_on": "has_batch_no", + "fieldname": "batches", + "fieldtype": "Table", + "label": "Batches", + "options": "Work Order Batch", + "read_only": 1 } ], "icon": "fa fa-cogs", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2600790a59a..587204c3414 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -19,6 +19,8 @@ from frappe.utils.csvutils import getlink from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty from erpnext.utilities.transaction_base import validate_uom_is_integer from frappe.model.mapper import get_mapped_doc +from erpnext.stock.doctype.batch.batch import make_batch +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos class OverProductionError(frappe.ValidationError): pass class CapacityError(frappe.ValidationError): pass @@ -40,6 +42,7 @@ class WorkOrder(Document): self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order) def validate(self): + self.set("batches", []) self.validate_production_item() if self.bom_no: validate_bom_no(self.production_item, self.bom_no) @@ -235,6 +238,9 @@ class WorkOrder(Document): production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) + def before_submit(self): + self.create_serial_no_batch_no() + def on_submit(self): if not self.wip_warehouse: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) @@ -266,6 +272,67 @@ class WorkOrder(Document): self.update_planned_qty() self.update_ordered_qty() self.update_reserved_qty_for_production() + self.delete_auto_created_batch_and_serial_no() + + def create_serial_no_batch_no(self): + if not (self.has_serial_no or self.has_batch_no): return + + if not cint(frappe.db.get_single_value("Manufacturing Settings", + "make_serial_no_batch_from_work_order")): return + + if self.has_batch_no: + self.set("batches", []) + self.create_batch_for_finished_good() + + args = {"item_code": self.production_item} + + if self.has_serial_no: + self.make_serial_nos(args) + + def create_batch_for_finished_good(self): + total_qty = self.qty + if not self.batch_size: + self.batch_size = total_qty + + while total_qty > 0: + qty = self.batch_size + if self.batch_size >= total_qty: + qty = total_qty + + if total_qty > self.batch_size: + total_qty -= self.batch_size + else: + qty = total_qty + total_qty = 0 + + batch = make_batch(self.production_item) + self.append("batches", { + "batch_no": batch, + "qty": qty, + }) + + def delete_auto_created_batch_and_serial_no(self): + if self.serial_no: + for d in get_serial_nos(self.serial_no): + frappe.delete_doc("Serial No", d) + + for row in self.batches: + batch_no = row.batch_no + row.db_set("batch_no", None) + frappe.delete_doc("Batch", batch_no) + + def make_serial_nos(self, args): + serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") + if serial_no_series: + self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) + elif self.serial_no: + args.update({"serial_no": self.serial_no, "actual_qty": self.qty, "batch_no": self.batch_no}) + self.serial_no = auto_make_serial_nos(args) + + serial_nos_length = len(get_serial_nos(self.serial_no)) + if serial_nos_length != self.qty: + frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.") + .format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError) def create_job_card(self): manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") @@ -273,32 +340,51 @@ class WorkOrder(Document): enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 - for i, row in enumerate(self.operations): - self.set_operation_start_end_time(i, row) + for index, row in enumerate(self.operations): + qty = self.qty + i=0 + while qty > 0: + i += 1 + if not cint(frappe.db.get_value("Operation", + row.operation, "create_job_card_based_on_batch_size")): + row.batch_size = self.qty - if not row.workstation: - frappe.throw(_("Row {0}: select the workstation against the operation {1}") - .format(row.idx, row.operation)) + job_card_qty = row.batch_size + if row.batch_size and qty >= row.batch_size: + qty -= row.batch_size + elif qty > 0: + job_card_qty = qty - original_start_time = row.planned_start_time - job_card_doc = create_job_card(self, row, - enable_capacity_planning=enable_capacity_planning, auto_create=True) - - if enable_capacity_planning and job_card_doc: - row.planned_start_time = job_card_doc.time_logs[-1].from_time - row.planned_end_time = job_card_doc.time_logs[-1].to_time - - if date_diff(row.planned_start_time, original_start_time) > plan_days: - frappe.message_log.pop() - frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") - .format(plan_days, row.operation), CapacityError) - - row.db_update() + if job_card_qty > 0: + self.prepare_data_for_job_card(row, job_card_qty, index, + plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: self.db_set("planned_end_date", planned_end_date) + def prepare_data_for_job_card(self, row, job_card_qty, index, plan_days, enable_capacity_planning): + self.set_operation_start_end_time(index, row) + + if not row.workstation: + frappe.throw(_("Row {0}: select the workstation against the operation {1}") + .format(row.idx, row.operation)) + + original_start_time = row.planned_start_time + job_card_doc = create_job_card(self, row, qty=job_card_qty, + enable_capacity_planning=enable_capacity_planning, auto_create=True) + + if enable_capacity_planning and job_card_doc: + row.planned_start_time = job_card_doc.time_logs[-1].from_time + row.planned_end_time = job_card_doc.time_logs[-1].to_time + + if date_diff(row.planned_start_time, original_start_time) > plan_days: + frappe.message_log.pop() + frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") + .format(plan_days, row.operation), CapacityError) + + row.db_update() + def set_operation_start_end_time(self, idx, row): """Set start and end time for given operation. If first operation, set start as `planned_start_date`, else add time diff to end time of earlier operation.""" @@ -669,6 +755,15 @@ class WorkOrder(Document): bom.set_bom_material_details() return bom + def update_batch_qty(self): + if self.has_batch_no and self.batches: + for row in self.batches: + qty = frappe.get_all("Stock Entry Detail", fields = ["sum(transfer_qty)"], + filters = {"docstatus": 1, "batch_no": row.batch_no, "is_finished_item": 1}, as_list=1) + + if qty: + frappe.db.set_value("Work Order Batch", row.name, "produced_qty", flt(qty[0][0])) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): @@ -826,6 +921,7 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.set_stock_entry_type() stock_entry.get_items() + stock_entry.set_serial_no_batch_for_finished_good() return stock_entry.as_dict() @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/work_order_batch/__init__.py b/erpnext/manufacturing/doctype/work_order_batch/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json new file mode 100644 index 00000000000..ad667b7c397 --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "creation": "2021-01-04 16:42:39.347528", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "batch_no", + "qty", + "produced_qty" + ], + "fields": [ + { + "fieldname": "batch_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Batch No", + "options": "Batch" + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "default": "0", + "fieldname": "produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "no_copy": 1, + "print_hide": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-05 10:57:07.278399", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Batch", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py new file mode 100644 index 00000000000..cf3ec475caf --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WorkOrderBatch(Document): + pass diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 508e17c3409..07cf08a5bbf 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -308,4 +308,11 @@ def validate_serial_no_with_batch(serial_nos, item_code): message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" frappe.throw(_("There is no batch found against the {0}: {1}") - .format(message, serial_no_link)) \ No newline at end of file + .format(message, serial_no_link)) + +def make_batch(item_code): + if frappe.db.get_value("Item", item_code, "has_batch_no"): + doc = frappe.new_doc("Batch") + doc.item = item_code + doc.save() + return doc.name \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 66f8b63cb92..5fde35a8117 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -498,6 +498,7 @@ class StockEntry(StockController): d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) if not d.t_warehouse: outgoing_items_cost += flt(d.basic_amount) + return outgoing_items_cost def get_args_for_incoming_rate(self, item): @@ -854,6 +855,7 @@ class StockEntry(StockController): pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") + pro_doc.update_batch_qty() if not pro_doc.operations: pro_doc.set_actual_dates() @@ -1076,18 +1078,45 @@ class StockEntry(StockController): # in case of BOM to_warehouse = item.get("default_warehouse") + args = { + "to_warehouse": to_warehouse, + "from_warehouse": "", + "qty": self.fg_completed_qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item.stock_uom, + "expense_account": item.get("expense_account"), + "cost_center": item.get("buying_cost_center"), + "is_finished_item": 1 + } + + if self.work_order and self.pro_doc.batches: + self.set_batchwise_finished_goods(args, item) + else: + self.add_finisged_goods(args, item) + + def set_batchwise_finished_goods(self, args, item): + qty = flt(self.fg_completed_qty) + for row in self.pro_doc.batches: + batch_qty = flt(row.qty) - flt(row.produced_qty) + if not batch_qty: continue + + if qty <=0: + break + + fg_qty = batch_qty + if batch_qty >= qty: + fg_qty = qty + + qty -= batch_qty + args["qty"] = fg_qty + args["batch_no"] = row.batch_no + + self.add_finisged_goods(args, item) + + def add_finisged_goods(self, args, item): self.add_to_stock_entry_detail({ - item.name: { - "to_warehouse": to_warehouse, - "from_warehouse": "", - "qty": self.fg_completed_qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item.stock_uom, - "expense_account": item.get("expense_account"), - "cost_center": item.get("buying_cost_center"), - "is_finished_item": 1 - } + item.name: args }, bom_no = self.bom_no) def get_bom_raw_materials(self, qty): @@ -1524,6 +1553,36 @@ class StockEntry(StockController): material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + def set_serial_no_batch_for_finished_good(self): + args = {} + if self.pro_doc.serial_no or self.pro_doc.batch_no: + self.get_serial_nos_for_fg(args) + + for row in self.items: + if row.is_finished_item and row.item_code == self.pro_doc.production_item: + if args.get("serial_no"): + row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)]) + + def get_serial_nos_for_fg(self, args): + fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", + "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"] + + filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"], + ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]] + + stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) + + if self.pro_doc.serial_no: + args["serial_no"] = self.get_available_serial_nos(stock_entries) + + def get_available_serial_nos(self, stock_entries): + used_serial_nos = [] + for row in stock_entries: + if row.serial_no: + used_serial_nos.extend(get_serial_nos(row.serial_no)) + + return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): From fcab53b238d2f6c1e0587ed309bf2765f63c72ec Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 5 Jan 2021 15:55:09 +0530 Subject: [PATCH 242/376] fix: skip job card --- .../doctype/bom_operation/bom_operation.json | 10 +++- .../doctype/work_order/work_order.js | 16 +++--- .../doctype/work_order/work_order.json | 9 --- .../doctype/work_order/work_order.py | 57 ++++++++++--------- .../work_order/work_order_dashboard.py | 7 +++ .../doctype/work_order_batch/__init__.py | 0 .../work_order_batch/work_order_batch.json | 49 ---------------- .../work_order_batch/work_order_batch.py | 10 ---- .../work_order_operation.json | 17 +++++- erpnext/stock/doctype/batch/batch.json | 31 +++++++++- erpnext/stock/doctype/batch/batch.py | 10 ++-- .../stock/doctype/serial_no/serial_no.json | 11 +++- erpnext/stock/doctype/serial_no/serial_no.py | 17 +++--- .../stock/doctype/stock_entry/stock_entry.py | 17 ++++-- 14 files changed, 132 insertions(+), 129 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/work_order_batch/__init__.py delete mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json delete mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 57062b8ca4c..1330636198b 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -11,6 +11,7 @@ "workstation", "description", "col_break1", + "skip_job_card", "hour_rate", "time_in_mins", "operating_cost", @@ -117,13 +118,20 @@ "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "skip_job_card", + "fieldtype": "Check", + "label": "Skip Job Card" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-14 15:01:33.142869", + "modified": "2021-01-05 14:29:11.887888", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 601734914da..adf6453e2eb 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -242,13 +242,15 @@ frappe.ui.form.on("Work Order", { if(data.completed_qty != frm.doc.qty) { pending_qty = frm.doc.qty - flt(data.completed_qty); - dialog.fields_dict.operations.df.data.push({ - 'name': data.name, - 'operation': data.operation, - 'workstation': data.workstation, - 'qty': pending_qty, - 'pending_qty': pending_qty, - }); + if (pending_qty && !data.skip_job_card) { + dialog.fields_dict.operations.df.data.push({ + 'name': data.name, + 'operation': data.operation, + 'workstation': data.workstation, + 'qty': pending_qty, + 'pending_qty': pending_qty, + }); + } } }); dialog.fields_dict.operations.grid.refresh(); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index cb3c9421077..c80decb92eb 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -27,7 +27,6 @@ "column_break_17", "serial_no", "batch_size", - "batches", "settings_section", "allow_alternative_item", "use_multi_level_bom", @@ -535,14 +534,6 @@ "fieldname": "batch_size", "fieldtype": "Float", "label": "Batch Size" - }, - { - "depends_on": "has_batch_no", - "fieldname": "batches", - "fieldtype": "Table", - "label": "Batches", - "options": "Work Order Batch", - "read_only": 1 } ], "icon": "fa fa-cogs", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 587204c3414..23cc0904276 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -27,6 +27,7 @@ class CapacityError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass +class SerialNoQtyError(frappe.ValidationError): pass from six import string_types @@ -42,7 +43,6 @@ class WorkOrder(Document): self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order) def validate(self): - self.set("batches", []) self.validate_production_item() if self.bom_no: validate_bom_no(self.production_item, self.bom_no) @@ -281,10 +281,12 @@ class WorkOrder(Document): "make_serial_no_batch_from_work_order")): return if self.has_batch_no: - self.set("batches", []) self.create_batch_for_finished_good() - args = {"item_code": self.production_item} + args = { + "item_code": self.production_item, + "work_order": self.name + } if self.has_serial_no: self.make_serial_nos(args) @@ -305,29 +307,29 @@ class WorkOrder(Document): qty = total_qty total_qty = 0 - batch = make_batch(self.production_item) - self.append("batches", { - "batch_no": batch, - "qty": qty, - }) + make_batch(frappe._dict({ + "item": self.production_item, + "qty_to_produce": qty, + "reference_doctype": self.doctype, + "reference_name": self.name + })) def delete_auto_created_batch_and_serial_no(self): - if self.serial_no: - for d in get_serial_nos(self.serial_no): - frappe.delete_doc("Serial No", d) + for row in frappe.get_all("Serial No", filters = {"work_order": self.name}): + frappe.delete_doc("Serial No", row.name) + self.db_set("serial_no", "") - for row in self.batches: - batch_no = row.batch_no - row.db_set("batch_no", None) - frappe.delete_doc("Batch", batch_no) + for row in frappe.get_all("Batch", filters = {"reference_name": self.name}): + frappe.delete_doc("Batch", row.name) def make_serial_nos(self, args): serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") if serial_no_series: self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) - elif self.serial_no: - args.update({"serial_no": self.serial_no, "actual_qty": self.qty, "batch_no": self.batch_no}) - self.serial_no = auto_make_serial_nos(args) + + if self.serial_no: + args.update({"serial_no": self.serial_no, "actual_qty": self.qty}) + auto_make_serial_nos(args) serial_nos_length = len(get_serial_nos(self.serial_no)) if serial_nos_length != self.qty: @@ -341,6 +343,7 @@ class WorkOrder(Document): plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 for index, row in enumerate(self.operations): + if row.skip_job_card: continue qty = self.qty i=0 while qty > 0: @@ -493,7 +496,7 @@ class WorkOrder(Document): select operation, description, workstation, idx, base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id + "Pending" as status, parent as bom, batch_size, sequence_id, skip_job_card from `tabBOM Operation` where @@ -755,14 +758,16 @@ class WorkOrder(Document): bom.set_bom_material_details() return bom - def update_batch_qty(self): - if self.has_batch_no and self.batches: - for row in self.batches: - qty = frappe.get_all("Stock Entry Detail", fields = ["sum(transfer_qty)"], - filters = {"docstatus": 1, "batch_no": row.batch_no, "is_finished_item": 1}, as_list=1) + def update_batch_produced_qty(self, stock_entry_doc): + if not cint(frappe.db.get_single_value("Manufacturing Settings", + "make_serial_no_batch_from_work_order")): return - if qty: - frappe.db.set_value("Work Order Batch", row.name, "produced_qty", flt(qty[0][0])) + for row in stock_entry_doc.items: + if row.batch_no and (row.is_finished_item or row.is_scrap_item): + qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no}, + or_conditions= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"])[0][0] + + frappe.db.set_value("Batch", row.batch_no, "produced_qty", qty) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 87c090f99c3..9aa0715e7ff 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -4,10 +4,17 @@ from frappe import _ def get_data(): return { 'fieldname': 'work_order', + 'non_standard_fieldnames': { + 'Batch': 'reference_name' + }, 'transactions': [ { 'label': _('Transactions'), 'items': ['Stock Entry', 'Job Card', 'Pick List'] + }, + { + 'label': _('Reference'), + 'items': ['Serial No', 'Batch'] } ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_batch/__init__.py b/erpnext/manufacturing/doctype/work_order_batch/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json deleted file mode 100644 index ad667b7c397..00000000000 --- a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "actions": [], - "creation": "2021-01-04 16:42:39.347528", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "batch_no", - "qty", - "produced_qty" - ], - "fields": [ - { - "fieldname": "batch_no", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Batch No", - "options": "Batch" - }, - { - "fieldname": "qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Qty", - "non_negative": 1 - }, - { - "default": "0", - "fieldname": "produced_qty", - "fieldtype": "Float", - "label": "Produced Qty", - "no_copy": 1, - "print_hide": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-01-05 10:57:07.278399", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Work Order Batch", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py deleted file mode 100644 index cf3ec475caf..00000000000 --- a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class WorkOrderBatch(Document): - pass diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 8c5cde9a13c..b77690997cf 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -8,8 +8,10 @@ "details", "operation", "bom", - "sequence_id", + "column_break_4", + "skip_job_card", "description", + "sequence_id", "col_break1", "completed_qty", "status", @@ -195,12 +197,23 @@ "label": "Sequence ID", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "skip_job_card", + "fieldtype": "Check", + "label": "Skip Job Card" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-14 12:58:49.241252", + "modified": "2021-01-08 17:42:05.372163", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index 943cb3401ff..e6d2e1330b5 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "field:batch_id", "creation": "2013-03-05 14:50:38", @@ -25,7 +26,11 @@ "reference_doctype", "reference_name", "section_break_7", - "description" + "description", + "manufacturing_section", + "qty_to_produce", + "column_break_23", + "produced_qty" ], "fields": [ { @@ -160,13 +165,35 @@ "label": "Batch UOM", "options": "UOM", "read_only": 1 + }, + { + "fieldname": "manufacturing_section", + "fieldtype": "Section Break", + "label": "Manufacturing" + }, + { + "fieldname": "qty_to_produce", + "fieldtype": "Float", + "label": "Qty To Produce", + "read_only": 1 + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "fieldname": "produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "read_only": 1 } ], "icon": "fa fa-archive", "idx": 1, "image_field": "image", + "links": [], "max_attachments": 5, - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-01-07 11:10:09.149170", "modified_by": "Administrator", "module": "Stock", "name": "Batch", diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 07cf08a5bbf..bb5ad5c6fee 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -310,9 +310,7 @@ def validate_serial_no_with_batch(serial_nos, item_code): frappe.throw(_("There is no batch found against the {0}: {1}") .format(message, serial_no_link)) -def make_batch(item_code): - if frappe.db.get_value("Item", item_code, "has_batch_no"): - doc = frappe.new_doc("Batch") - doc.item = item_code - doc.save() - return doc.name \ No newline at end of file +def make_batch(args): + if frappe.db.get_value("Item", args.item, "has_batch_no"): + args.doctype = "Batch" + frappe.get_doc(args).insert().name \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 3acf3a9316c..a3d44af4945 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -57,7 +57,8 @@ "more_info", "serial_no_details", "company", - "status" + "status", + "work_order" ], "fields": [ { @@ -422,12 +423,18 @@ "label": "Status", "options": "\nActive\nInactive\nDelivered\nExpired", "read_only": 1 + }, + { + "fieldname": "work_order", + "fieldtype": "Link", + "label": "Work Order", + "options": "Work Order" } ], "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2020-07-20 20:50:16.660433", + "modified": "2021-01-08 14:31:15.375996", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index b236f6a9990..bad7b608acf 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -473,16 +473,13 @@ def get_serial_nos(serial_no): if s.strip()] def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): - serial_no_doc.update({ - "item_code": args.get("item_code"), - "company": args.get("company"), - "batch_no": args.get("batch_no"), - "via_stock_ledger": args.get("via_stock_ledger") or True, - "supplier": args.get("supplier"), - "location": args.get("location"), - "warehouse": (args.get("warehouse") - if args.get("actual_qty", 0) > 0 else None) - }) + for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: + if args.get(field): + serial_no_doc.set(field, args.get(field)) + + serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True + serial_no_doc.warehouse = (args.get("warehouse") + if args.get("actual_qty", 0) > 0 else None) if is_new: serial_no_doc.serial_no = serial_no diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5fde35a8117..83412c61d99 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -855,7 +855,7 @@ class StockEntry(StockController): pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") - pro_doc.update_batch_qty() + pro_doc.update_batch_produced_qty(self) if not pro_doc.operations: pro_doc.set_actual_dates() @@ -1090,14 +1090,21 @@ class StockEntry(StockController): "is_finished_item": 1 } - if self.work_order and self.pro_doc.batches: + if self.work_order and self.pro_doc.has_batch_no: self.set_batchwise_finished_goods(args, item) else: self.add_finisged_goods(args, item) def set_batchwise_finished_goods(self, args, item): qty = flt(self.fg_completed_qty) - for row in self.pro_doc.batches: + filters = {"reference_name": self.pro_doc.name, + "reference_doctype": self.pro_doc.doctype, + "qty_to_produce": (">", 0) + } + + fields = ["qty_to_produce as qty", "produced_qty", "name"] + + for row in frappe.get_all("Batch", filters = filters, fields = fields): batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue @@ -1110,7 +1117,7 @@ class StockEntry(StockController): qty -= batch_qty args["qty"] = fg_qty - args["batch_no"] = row.batch_no + args["batch_no"] = row.name self.add_finisged_goods(args, item) @@ -1555,7 +1562,7 @@ class StockEntry(StockController): def set_serial_no_batch_for_finished_good(self): args = {} - if self.pro_doc.serial_no or self.pro_doc.batch_no: + if self.pro_doc.serial_no: self.get_serial_nos_for_fg(args) for row in self.items: From 6a9798f305d93a879be5264e6388b36b04b7ec43 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 24 Jun 2021 18:11:33 +0530 Subject: [PATCH 243/376] fix: update leave allocation after submit (#26191) * fix: update leave allocation after submit v13 * fix: test * fix: test --- .../leave_allocation/leave_allocation.json | 5 +- .../leave_allocation/leave_allocation.py | 40 +++++++++++++++- .../leave_allocation/test_leave_allocation.py | 46 +++++++++++++++++++ .../employee_leave_balance.py | 2 +- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index ae02c512c23..3a6539ece9e 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -110,6 +110,7 @@ "label": "Allocation" }, { + "allow_on_submit": 1, "bold": 1, "fieldname": "new_leaves_allocated", "fieldtype": "Float", @@ -235,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-14 15:28:26.335104", + "modified": "2021-06-03 15:28:26.335104", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -277,4 +278,4 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "employee" -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 11302cad758..4757cd3b192 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry +from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period class OverlapError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass @@ -55,6 +56,43 @@ class LeaveAllocation(Document): if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) + def on_update_after_submit(self): + if self.has_value_changed("new_leaves_allocated"): + self.validate_against_leave_applications() + leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count() + args = { + "leaves": leaves_to_be_added, + "from_date": self.from_date, + "to_date": self.to_date, + "is_carry_forward": 0 + } + create_leave_ledger_entry(self, args, True) + + def get_existing_leave_count(self): + ledger_entries = frappe.get_all("Leave Ledger Entry", + filters={ + "transaction_type": "Leave Allocation", + "transaction_name": self.name, + "employee": self.employee, + "company": self.company, + "leave_type": self.leave_type + }, + pluck="leaves") + total_existing_leaves = 0 + for entry in ledger_entries: + total_existing_leaves += entry + + return total_existing_leaves + + def validate_against_leave_applications(self): + leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, + self.from_date, self.to_date) + if flt(leaves_taken) > flt(self.total_leaves_allocated): + if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): + frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) + else: + frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) + def update_leave_policy_assignments_when_no_allocations_left(self): allocations = frappe.db.get_list("Leave Allocation", filters = { "docstatus": 1, @@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date): def validate_carry_forward(leave_type): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): - frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) \ No newline at end of file + frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 6e7ae87d08c..bff06e6a91c 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import frappe +import erpnext import unittest from frappe.utils import nowdate, add_months, getdate, add_days from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type @@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.cancel() self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) + def test_leave_addition_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 40 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 40) + + def test_leave_subtraction_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 10 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 10) + + def test_against_leave_application_validation_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + leave_application = frappe.get_doc({ + "doctype": 'Leave Application', + "employee": employee.name, + "leave_type": "_Test Leave Type", + "from_date": add_months(nowdate(), 2), + "to_date": add_months(add_days(nowdate(), 10), 2), + "company": erpnext.get_default_company() or "_Test Company", + "docstatus": 1, + "status": "Approved", + "leave_approver": 'test@example.com' + }) + leave_application.submit() + leave_allocation.new_leaves_allocated = 8 + leave_allocation.total_leaves_allocated = 8 + self.assertRaises(frappe.ValidationError, leave_allocation.submit) + def create_leave_allocation(**args): args = frappe._dict(args) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 4dd4570e8ca..b8953b3eaac 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 AND leaves>0 + AND docstatus=1 AND (from_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s OR (from_date < %(from_date)s AND to_date > %(to_date)s)) From c878389050a45fa6cdebf057da1752242db0ad3f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 8 Jan 2021 19:47:38 +0530 Subject: [PATCH 244/376] fix: or condition filter in the get_all --- .../doctype/job_card/job_card.json | 8 +- .../doctype/job_card/job_card.py | 20 +-- .../doctype/job_card_item/job_card_item.json | 13 ++ .../doctype/work_order/work_order.py | 9 +- .../cost_of_poor_quality_report/__init__.py | 0 .../cost_of_poor_quality_report.js | 9 ++ .../cost_of_poor_quality_report.json | 33 +++++ .../cost_of_poor_quality_report.py | 136 ++++++++++++++++++ erpnext/patches.txt | 3 +- .../patches/v13_0/update_job_card_details.py | 16 +++ .../stock/doctype/stock_entry/stock_entry.py | 3 +- 11 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py create mode 100644 erpnext/patches/v13_0/update_job_card_details.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index c2fd8cc3f92..0597cdb2077 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -33,6 +33,7 @@ "total_completed_qty", "column_break_15", "total_time_in_mins", + "hour_rate", "section_break_8", "items", "more_information", @@ -328,11 +329,16 @@ "fieldname": "section_break_21", "fieldtype": "Section Break", "hide_border": 1 + }, + { + "fieldname": "hour_rate", + "fieldtype": "Currency", + "label": "Hour Rate" } ], "is_submittable": 1, "links": [], - "modified": "2020-12-14 15:14:05.566271", + "modified": "2021-01-11 12:09:00.452032", "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 5c157d43ec8..b2d56673689 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -41,6 +41,7 @@ class JobCard(Document): def validate_time_logs(self): self.total_time_in_mins = 0.0 + self.total_completed_qty = 0.0 if self.get('time_logs'): for d in self.get('time_logs'): @@ -58,8 +59,6 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty - else: - self.total_completed_qty = 0.0 self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) @@ -256,12 +255,14 @@ class JobCard(Document): if self.get('operation') == d.operation: self.append('items', { - 'item_code': d.item_code, - 'source_warehouse': d.source_warehouse, - 'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), - 'item_name': d.item_name, - 'description': d.description, - 'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty + "item_code": d.item_code, + "source_warehouse": d.source_warehouse, + "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'), + "item_name": d.item_name, + "description": d.description, + "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, + "rate": d.rate, + "amount": d.amount }) def on_submit(self): @@ -439,7 +440,8 @@ class JobCard(Document): data = frappe.get_all("Work Order Operation", fields = ["operation", "status", "completed_qty"], - filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)}, + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id), + "skip_job_card": 0}, order_by = "sequence_id, idx") message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name), diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 100ef4ca3a3..60a2249442a 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -17,6 +17,8 @@ "required_qty", "column_break_9", "transferred_qty", + "rate", + "amount", "allow_alternative_item" ], "fields": [ @@ -101,6 +103,17 @@ "label": "Transferred Qty", "no_copy": 1, "print_hide": 1, + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", "read_only": 1 } ], diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 23cc0904276..06cafd2d04c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -764,10 +764,10 @@ class WorkOrder(Document): for row in stock_entry_doc.items: if row.batch_no and (row.is_finished_item or row.is_scrap_item): - qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no}, - or_conditions= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"])[0][0] + qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1}, + or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0] - frappe.db.set_value("Batch", row.batch_no, "produced_qty", qty) + frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty)) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -1006,7 +1006,8 @@ def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto 'project': work_order.project, 'company': work_order.company, 'sequence_id': row.get("sequence_id"), - 'wip_warehouse': work_order.wip_warehouse + 'wip_warehouse': work_order.wip_warehouse, + "hour_rate": row.get("hour_rate") }) if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js new file mode 100644 index 00000000000..7f5bc48f18e --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -0,0 +1,9 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Cost of Poor Quality Report"] = { + "filters": [ + + ] +}; diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json new file mode 100644 index 00000000000..ee63bc1c287 --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-01-11 11:10:58.292896", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2021-01-11 11:11:03.594242", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Cost of Poor Quality Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Job Card", + "report_name": "Cost of Poor Quality Report", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py new file mode 100644 index 00000000000..21e7be7478d --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -0,0 +1,136 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt + +def execute(filters=None): + columns, data = [], [] + + columns = get_columns(filters) + data = get_data(filters) + + return columns, data + +def get_data(filters): + data = [] + operations = frappe.get_all("Operation", filters = {"cost_of_poor_quality_operation": 1}) + if operations: + operations = [d.name for d in operations] + fields = ["production_item as item_code", "item_name", "work_order", "operation", + "workstation", "total_time_in_mins", "name", "hour_rate"] + + job_cards = frappe.get_all("Job Card", fields = fields, + filters = {"docstatus": 1, "operation": ("in", operations)}) + + for row in job_cards: + row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) + update_raw_material_cost(row, filters) + update_time_details(row, filters, data) + + return data + +def update_raw_material_cost(row, filters): + row.rm_cost = 0.0 + for data in frappe.get_all("Job Card Item", fields = ["amount"], + filters={"parent": row.name, "docstatus": 1}): + row.rm_cost += data.amount + +def update_time_details(row, filters, data): + args = frappe._dict({"item_code": "", "item_name": "", "name": "", "work_order":"", + "operation": "", "workstation":"", "operating_cost": "", "rm_cost": "", "total_time_in_mins": ""}) + + i=0 + for time_log in frappe.get_all("Job Card Time Log", fields = ["from_time", "to_time", "time_in_mins"], + filters={"parent": row.name, "docstatus": 1}): + + if i==0: + i += 1 + row.update(time_log) + data.append(row) + else: + args.update(time_log) + data.append(args) + +def get_columns(filters): + return [ + { + "label": _("Job Card"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Job Card", + "width": "100" + }, + { + "label": _("Work Order"), + "fieldtype": "Link", + "fieldname": "work_order", + "options": "Work Order", + "width": "100" + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": "100" + }, + { + "label": _("Item Name"), + "fieldtype": "Data", + "fieldname": "item_name", + "width": "100" + }, + { + "label": _("Operation"), + "fieldtype": "Link", + "fieldname": "operation", + "options": "Operation", + "width": "100" + }, + { + "label": _("Workstation"), + "fieldtype": "Link", + "fieldname": "workstation", + "options": "Workstation", + "width": "100" + }, + { + "label": _("Operating Cost"), + "fieldtype": "Currency", + "fieldname": "operating_cost", + "width": "100" + }, + { + "label": _("Raw Material Cost"), + "fieldtype": "Currency", + "fieldname": "rm_cost", + "width": "100" + }, + { + "label": _("Total Time (in Mins)"), + "fieldtype": "Float", + "fieldname": "total_time_in_mins", + "width": "100" + }, + { + "label": _("From Time"), + "fieldtype": "Datetime", + "fieldname": "from_time", + "width": "100" + }, + { + "label": _("To Time"), + "fieldtype": "Datetime", + "fieldname": "to_time", + "width": "100" + }, + { + "label": _("Time in Mins"), + "fieldtype": "Float", + "fieldname": "time_in_mins", + "width": "100" + }, + ] \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index dd0e33bebae..2b1fc43a1c0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -288,4 +288,5 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold -erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice \ No newline at end of file +erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice +erpnext.patches.v13_0.update_job_card_details diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py new file mode 100644 index 00000000000..d4e65c6f2f2 --- /dev/null +++ b/erpnext/patches/v13_0/update_job_card_details.py @@ -0,0 +1,16 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "job_card") + frappe.reload_doc("manufacturing", "doctype", "job_card_item") + frappe.reload_doc("manufacturing", "doctype", "work_order_operation") + + frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo + SET jc.hour_rate = wo.hour_rate + WHERE + jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0 + """) \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 83412c61d99..4cc721badf1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -365,6 +365,7 @@ class StockEntry(StockController): "overproduction_percentage_for_work_order")) for d in prod_order.get("operations"): + if d.skip_job_card: continue total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty) if total_completed_qty > flt(completed_qty): @@ -1104,7 +1105,7 @@ class StockEntry(StockController): fields = ["qty_to_produce as qty", "produced_qty", "name"] - for row in frappe.get_all("Batch", filters = filters, fields = fields): + for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue From 57307443f04c7645889e9e8f41670f18f9ba63ee Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Jan 2021 18:32:33 +0530 Subject: [PATCH 245/376] is corrective job card --- .../doctype/bom_operation/bom_operation.json | 10 +-- .../doctype/job_card/job_card.js | 53 +++++++++++-- .../doctype/job_card/job_card.json | 43 +++++++++- .../doctype/job_card/job_card.py | 78 +++++++++++++++---- .../doctype/job_card_item/job_card_item.json | 2 +- .../doctype/operation/operation.json | 17 ++-- .../doctype/work_order/work_order.js | 4 +- .../doctype/work_order/work_order.json | 11 +++ .../doctype/work_order/work_order.py | 8 +- .../work_order_operation.json | 10 +-- .../cost_of_poor_quality_report.js | 62 ++++++++++++++- .../cost_of_poor_quality_report.py | 26 +++++-- .../stock/doctype/stock_entry/stock_entry.py | 1 - 13 files changed, 260 insertions(+), 65 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 1330636198b..4458e6db234 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -11,7 +11,6 @@ "workstation", "description", "col_break1", - "skip_job_card", "hour_rate", "time_in_mins", "operating_cost", @@ -118,20 +117,13 @@ "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID" - }, - { - "allow_on_submit": 1, - "default": "0", - "fieldname": "skip_job_card", - "fieldtype": "Check", - "label": "Skip Job Card" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-05 14:29:11.887888", + "modified": "2021-01-12 14:48:09.596843", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 57ec20b42c9..266d5f6058c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -41,6 +41,10 @@ frappe.ui.form.on('Job Card', { } } + if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { + frm.trigger('setup_corrective_job_card') + } + frm.set_query("quality_inspection", function() { return { query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", @@ -53,12 +57,50 @@ frappe.ui.form.on('Job Card', { frm.trigger("toggle_operation_number"); - if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) + if (frm.doc.docstatus == 0 && !frm.is_new() && + (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, + setup_corrective_job_card: function(frm) { + frm.add_custom_button(__('Corrective Job Card'), () => { + let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation); + + let fields = [ + { + fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', + fieldname: 'operation', get_query() { return { filters: { "is_corrective_operation": 1 }}} + }, { + fieldtype: 'Link', label: __('For Operation'), options: 'Operation', + fieldname: 'for_operation', get_query() { return { filters: { "name": ["in", operations] }}} + } + ]; + + frappe.prompt(fields, d => { + frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); + }, __("Select Corrective Operation")); + }, __('Make')); + }, + + make_corrective_job_card: function(frm, operation, for_operation) { + frappe.call({ + method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card', + args: { + source_name: frm.doc.name, + operation: operation, + for_operation: for_operation + }, + callback: function(r) { + if (r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }); + }, + operation: function(frm) { frm.trigger("toggle_operation_number"); @@ -110,10 +152,9 @@ frappe.ui.form.on('Job Card', { if (!frm.doc.started_time && !frm.doc.current_time) { frm.add_custom_button(__("Start Job"), () => { - frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Employee'), options: "Job Card Time Log", - fieldname: 'employee'}, d => { - debugger - frm.events.start_job(frm, "Work In Progress", d.employee); + frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), + options: "Job Card Time Log", fieldname: 'employees'}, d => { + frm.events.start_job(frm, "Work In Progress", d.employees); }, __("Assign Job to Employee")); }).addClass("btn-primary"); } else if (frm.doc.status == "On Hold") { @@ -138,7 +179,7 @@ frappe.ui.form.on('Job Card', { const args = { job_card_id: frm.doc.name, start_time: frappe.datetime.now_datetime(), - employee: employee, + employees: employee, status: status }; frm.events.make_time_log(frm, args); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 0597cdb2077..be7a8101734 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -33,9 +33,14 @@ "total_completed_qty", "column_break_15", "total_time_in_mins", - "hour_rate", "section_break_8", "items", + "corrective_operation_section", + "for_job_card", + "is_corrective_job_card", + "column_break_33", + "hour_rate", + "for_operation", "more_information", "operation_id", "sequence_id", @@ -331,14 +336,48 @@ "hide_border": 1 }, { + "depends_on": "is_corrective_job_card", "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate" + }, + { + "collapsible": 1, + "depends_on": "is_corrective_job_card", + "fieldname": "corrective_operation_section", + "fieldtype": "Section Break", + "label": "Corrective Operation" + }, + { + "default": "0", + "fieldname": "is_corrective_job_card", + "fieldtype": "Check", + "label": "Is Corrective Job Card", + "read_only": 1 + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "fieldname": "for_job_card", + "fieldtype": "Link", + "label": "For Job Card", + "options": "Job Card", + "read_only": 1 + }, + { + "fetch_from": "for_job_card.operation", + "fetch_if_empty": 1, + "fieldname": "for_operation", + "fieldtype": "Link", + "label": "For Operation", + "options": "Operation" } ], "is_submittable": 1, "links": [], - "modified": "2021-01-11 12:09:00.452032", + "modified": "2021-02-03 20:36:51.826944", "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 b2d56673689..b4202e158d4 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -178,18 +178,27 @@ class JobCard(Document): self.reset_timer_value(args) if last_row and args.get("complete_time"): - last_row.update({ - "to_time": get_datetime(args.get("complete_time")), - "operation": args.get("sub_operation"), - "completed_qty": args.get("completed_qty") or 0.0 - }) + for row in self.time_logs: + if not row.to_time: + row.update({ + "to_time": get_datetime(args.get("complete_time")), + "operation": args.get("sub_operation"), + "completed_qty": args.get("completed_qty") or 0.0 + }) elif args.get("start_time"): - self.append("time_logs", { - "from_time": get_datetime(args.get("start_time")), - "employee": args.get("employee"), - "operation": args.get("sub_operation"), - "completed_qty": 0.0 - }) + employees = args.employees + print(args) + if isinstance(employees, string_types): + employees = json.loads(employees) + + for name in employees: + print(name.get('employee')) + self.append("time_logs", { + "from_time": get_datetime(args.get("start_time")), + "employee": name.get('employee'), + "operation": args.get("sub_operation"), + "completed_qty": 0.0 + }) if self.status == "On Hold": self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) @@ -300,10 +309,24 @@ class JobCard(Document): time_in_mins = flt(data[0].time_in_mins) wo = frappe.get_doc('Work Order', self.work_order) - if self.operation_id: + + if self.is_corrective_job_card: + self.update_corrective_in_work_order(wo) + + elif self.operation_id: self.validate_produced_quantity(for_quantity, wo) self.update_work_order_data(for_quantity, time_in_mins, wo) + def update_corrective_in_work_order(self, wo): + wo.corrective_operation_cost = 0.0 + for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'], + filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}): + wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) + + wo.calculate_operating_cost() + wo.flags.ignore_validate_update_after_submit = True + wo.save() + def validate_produced_quantity(self, for_quantity, wo): if self.docstatus < 2: return @@ -346,7 +369,8 @@ class JobCard(Document): def get_current_operation_data(self): return frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) + filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id, + "is_corrective_job_card": 0}) def set_transferred_qty_in_job_card(self, ste_doc): for row in ste_doc.items: @@ -429,6 +453,8 @@ class JobCard(Document): .format(bold(self.operation), work_order), OperationMismatchError) def validate_sequence_id(self): + if self.is_corrective_job_card: return + if not (self.work_order and self.sequence_id): return current_operation_qty = 0.0 @@ -440,8 +466,7 @@ class JobCard(Document): data = frappe.get_all("Work Order Operation", fields = ["operation", "status", "completed_qty"], - filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id), - "skip_job_card": 0}, + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)}, order_by = "sequence_id, idx") message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name), @@ -598,3 +623,26 @@ def get_job_details(start, end, filters=None): events.append(job_card_data) return events + +@frappe.whitelist() +def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None): + def set_missing_values(source, target): + target.is_corrective_job_card = 1 + target.operation = operation + target.for_operation = for_operation + + target.set('time_logs', []) + target.get_sub_operations() + target.get_required_items() + target.validate_time_logs() + + doclist = get_mapped_doc("Job Card", source_name, { + "Job Card": { + "doctype": "Job Card", + "field_map": { + "name": "for_job_card", + }, + } + }, target_doc, set_missing_values) + + return doclist \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 60a2249442a..a239a247e33 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -102,7 +102,7 @@ "fieldtype": "Float", "label": "Transferred Qty", "no_copy": 1, - "print_hide": 1, + "print_hide": 1 }, { "fieldname": "rate", diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json index 9e6f8e1f5d7..10a97eda763 100644 --- a/erpnext/manufacturing/doctype/operation/operation.json +++ b/erpnext/manufacturing/doctype/operation/operation.json @@ -10,7 +10,7 @@ "field_order": [ "workstation", "data_2", - "cost_of_poor_quality_operation", + "is_corrective_operation", "job_card_section", "create_job_card_based_on_batch_size", "column_break_6", @@ -77,13 +77,6 @@ "fieldtype": "Check", "label": "Create Job Card based on Batch Size" }, - { - "default": "0", - "description": "Cost of poor quality operation", - "fieldname": "cost_of_poor_quality_operation", - "fieldtype": "Check", - "label": "Is COPQ Operation" - }, { "collapsible": 1, "fieldname": "job_card_section", @@ -93,12 +86,18 @@ { "fieldname": "column_break_6", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_corrective_operation", + "fieldtype": "Check", + "label": "Is Corrective Operation" } ], "icon": "fa fa-wrench", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-24 14:25:03.428303", + "modified": "2021-01-12 15:09:23.593338", "modified_by": "Administrator", "module": "Manufacturing", "name": "Operation", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index adf6453e2eb..acb3407e2bb 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -242,13 +242,13 @@ frappe.ui.form.on("Work Order", { if(data.completed_qty != frm.doc.qty) { pending_qty = frm.doc.qty - flt(data.completed_qty); - if (pending_qty && !data.skip_job_card) { + if (pending_qty) { dialog.fields_dict.operations.df.data.push({ 'name': data.name, 'operation': data.operation, 'workstation': data.workstation, 'qty': pending_qty, - 'pending_qty': pending_qty, + 'pending_qty': pending_qty }); } } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index c80decb92eb..8e99c665f1c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -58,6 +58,7 @@ "actual_operating_cost", "additional_operating_cost", "column_break_24", + "corrective_operation_cost", "total_operating_cost", "more_info", "description", @@ -534,6 +535,16 @@ "fieldname": "batch_size", "fieldtype": "Float", "label": "Batch Size" + }, + { + "allow_on_submit": 1, + "description": "From Corrective Job Card", + "fieldname": "corrective_operation_cost", + "fieldtype": "Currency", + "label": "Corrective Operation Cost", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-cogs", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 06cafd2d04c..c83f539e034 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -130,7 +130,9 @@ class WorkOrder(Document): variable_cost = self.actual_operating_cost if self.actual_operating_cost \ else self.planned_operating_cost - self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost) + + self.total_operating_cost = (flt(self.additional_operating_cost) + + flt(variable_cost) + flt(self.corrective_operation_cost)) def validate_work_order_against_so(self): # already ordered qty @@ -343,7 +345,6 @@ class WorkOrder(Document): plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 for index, row in enumerate(self.operations): - if row.skip_job_card: continue qty = self.qty i=0 while qty > 0: @@ -357,6 +358,7 @@ class WorkOrder(Document): qty -= row.batch_size elif qty > 0: job_card_qty = qty + qty = 0 if job_card_qty > 0: self.prepare_data_for_job_card(row, job_card_qty, index, @@ -496,7 +498,7 @@ class WorkOrder(Document): select operation, description, workstation, idx, base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id, skip_job_card + "Pending" as status, parent as bom, batch_size, sequence_id from `tabBOM Operation` where diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index b77690997cf..6d8fb80e319 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -9,7 +9,6 @@ "operation", "bom", "column_break_4", - "skip_job_card", "description", "sequence_id", "col_break1", @@ -201,19 +200,12 @@ { "fieldname": "column_break_4", "fieldtype": "Column Break" - }, - { - "allow_on_submit": 1, - "default": "0", - "fieldname": "skip_job_card", - "fieldtype": "Check", - "label": "Skip Job Card" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-08 17:42:05.372163", + "modified": "2021-01-12 14:48:31.061286", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js index 7f5bc48f18e..ef775663893 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -4,6 +4,66 @@ frappe.query_reports["Cost of Poor Quality Report"] = { "filters": [ - + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Datetime", + default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Datetime", + default: frappe.datetime.now_datetime(), + reqd: 1, + }, + { + label: __("Job Card"), + fieldname: "name", + fieldtype: "Link", + options: "Job Card", + get_query: function() { + return { + filters: { + is_corrective_job_card: 1, + docstatus: 1 + } + } + } + }, + { + label: __("Work Order"), + fieldname: "work_order", + fieldtype: "Link", + options: "Work Order" + }, + { + label: __("Operation"), + fieldname: "operation", + fieldtype: "Link", + options: "Operation", + get_query: function() { + return { + filters: { + is_corrective_operation: 1 + } + } + } + }, + { + label: __("Workstation"), + fieldname: "workstation", + fieldtype: "Link", + options: "Workstation" + }, ] }; diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 21e7be7478d..2e8c191c608 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -14,24 +14,34 @@ def execute(filters=None): return columns, data -def get_data(filters): +def get_data(report_filters): data = [] - operations = frappe.get_all("Operation", filters = {"cost_of_poor_quality_operation": 1}) + operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1}) if operations: operations = [d.name for d in operations] fields = ["production_item as item_code", "item_name", "work_order", "operation", "workstation", "total_time_in_mins", "name", "hour_rate"] + filters = get_filters(report_filters, operations) + job_cards = frappe.get_all("Job Card", fields = fields, - filters = {"docstatus": 1, "operation": ("in", operations)}) + filters = filters) for row in job_cards: row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) - update_raw_material_cost(row, filters) - update_time_details(row, filters, data) + update_raw_material_cost(row, report_filters) + update_time_details(row, report_filters, data) return data +def get_filters(report_filters, operations): + filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} + for field in ["name", "work_order", "operation", "workstation", "company"]: + if report_filters.get(field): + filters[field] = report_filters.get(field) + + return filters + def update_raw_material_cost(row, filters): row.rm_cost = 0.0 for data in frappe.get_all("Job Card Item", fields = ["amount"], @@ -43,8 +53,10 @@ def update_time_details(row, filters, data): "operation": "", "workstation":"", "operating_cost": "", "rm_cost": "", "total_time_in_mins": ""}) i=0 - for time_log in frappe.get_all("Job Card Time Log", fields = ["from_time", "to_time", "time_in_mins"], - filters={"parent": row.name, "docstatus": 1}): + for time_log in frappe.get_all("Job Card Time Log", + fields = ["from_time", "to_time", "time_in_mins"], + filters={"parent": row.name, "docstatus": 1, + "from_time": (">=", filters.from_date), "to_time": ("<=", filters.to_date)}): if i==0: i += 1 diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4cc721badf1..e49c9a57c3e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -365,7 +365,6 @@ class StockEntry(StockController): "overproduction_percentage_for_work_order")) for d in prod_order.get("operations"): - if d.skip_job_card: continue total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty) if total_completed_qty > flt(completed_qty): From 2330c41ccae1777c063a14024c4cdd42aeb9c921 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 17 Mar 2021 14:03:12 +0530 Subject: [PATCH 246/376] fix: total time calculation --- erpnext/manufacturing/doctype/bom/bom.js | 8 -- erpnext/manufacturing/doctype/bom/bom.json | 5 +- erpnext/manufacturing/doctype/bom/bom.py | 2 +- .../doctype/job_card/job_card.js | 56 ++++++++--- .../doctype/job_card/job_card.json | 25 ++++- .../doctype/job_card/job_card.py | 89 ++++++++++++----- .../doctype/job_card_item/job_card_item.json | 23 +---- .../job_card_operation.json | 11 ++- .../manufacturing_settings.json | 9 +- .../doctype/operation/operation.js | 10 +- .../doctype/operation/operation.py | 1 - .../doctype/work_order/work_order.js | 43 +++++--- .../doctype/work_order/work_order.json | 5 +- .../doctype/work_order/work_order.py | 97 ++++++++++++------- .../cost_of_poor_quality_report.js | 36 +++++++ .../cost_of_poor_quality_report.py | 61 ++++-------- .../stock/doctype/stock_entry/stock_entry.py | 10 +- .../stock_entry_detail.json | 4 +- 18 files changed, 324 insertions(+), 171 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index a09a5e34300..27019dbbae2 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", { refresh: function(frm) { frm.toggle_enable("item", frm.doc.__islocal); - toggle_operations(frm); frm.set_indicator_formatter('item_code', function(doc) { @@ -651,15 +650,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) { erpnext.bom.calculate_total(frm.doc); }); -var toggle_operations = function(frm) { - frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1); - frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1); - frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1); -}; - frappe.ui.form.on("BOM", "with_operations", function(frm) { if(!cint(frm.doc.with_operations)) { frm.set_value("operations", []); } - toggle_operations(frm); }); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index f551b91597f..f38d1b98922 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -193,6 +193,7 @@ }, { "default": "Work Order", + "depends_on": "with_operations", "fieldname": "transfer_material_against", "fieldtype": "Select", "label": "Transfer Material Against", @@ -235,6 +236,7 @@ { "fieldname": "operations_section", "fieldtype": "Section Break", + "hide_border": 1, "oldfieldtype": "Section Break" }, { @@ -245,6 +247,7 @@ "options": "Routing" }, { + "depends_on": "with_operations", "fieldname": "operations", "fieldtype": "Table", "label": "Operations", @@ -517,7 +520,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-05-21 12:29:32.634952", + "modified": "2021-03-16 12:25:09.081968", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3f109d91b5e..3e855603b48 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -590,7 +590,7 @@ class BOM(WebsiteGenerator): self.get_routing() def validate_operations(self): - if self.with_operations and not self.get('operations'): + if self.with_operations and not self.get('operations') and self.docstatus == 1: frappe.throw(_("Operations cannot be left blank")) if self.with_operations: diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 266d5f6058c..81860c9fbcf 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -42,7 +42,7 @@ frappe.ui.form.on('Job Card', { } if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { - frm.trigger('setup_corrective_job_card') + frm.trigger('setup_corrective_job_card'); } frm.set_query("quality_inspection", function() { @@ -71,15 +71,27 @@ frappe.ui.form.on('Job Card', { let fields = [ { fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', - fieldname: 'operation', get_query() { return { filters: { "is_corrective_operation": 1 }}} + fieldname: 'operation', get_query() { + return { + filters: { + "is_corrective_operation": 1 + } + }; + } }, { fieldtype: 'Link', label: __('For Operation'), options: 'Operation', - fieldname: 'for_operation', get_query() { return { filters: { "name": ["in", operations] }}} + fieldname: 'for_operation', get_query() { + return { + filters: { + "name": ["in", operations] + } + }; + } } ]; frappe.prompt(fields, d => { - frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); + frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); }, __("Select Corrective Operation")); }, __('Make')); }, @@ -152,14 +164,18 @@ frappe.ui.form.on('Job Card', { if (!frm.doc.started_time && !frm.doc.current_time) { frm.add_custom_button(__("Start Job"), () => { - frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), - options: "Job Card Time Log", fieldname: 'employees'}, d => { + if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) { + frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), + options: "Job Card Time Log", fieldname: 'employees'}, d => { frm.events.start_job(frm, "Work In Progress", d.employees); - }, __("Assign Job to Employee")); + }, __("Assign Job to Employee")); + } else { + frm.events.start_job(frm, "Work In Progress", frm.doc.employee); + } }).addClass("btn-primary"); } else if (frm.doc.status == "On Hold") { frm.add_custom_button(__("Resume Job"), () => { - frm.events.start_job(frm, "Resume Job"); + frm.events.start_job(frm, "Resume Job", frm.doc.employee); }).addClass("btn-primary"); } else { frm.add_custom_button(__("Pause Job"), () => { @@ -167,10 +183,26 @@ frappe.ui.form.on('Job Card', { }); frm.add_custom_button(__("Complete Job"), () => { - frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), - fieldname: 'qty', default: frm.doc.for_quantity}, data => { + var sub_operations = frm.doc.sub_operations; + + let set_qty = true; + if (sub_operations && sub_operations.length > 1) { + set_qty = false; + let last_op_row = sub_operations[sub_operations.length - 2]; + + if (last_op_row.status == 'Complete') { + set_qty = true; + } + } + + if (set_qty) { + frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), + fieldname: 'qty', default: frm.doc.for_quantity}, data => { frm.events.complete_job(frm, "Complete", data.qty); }, __("Enter Value")); + } else { + frm.events.complete_job(frm, "Complete", 0.0); + } }).addClass("btn-primary"); } }, @@ -204,11 +236,11 @@ frappe.ui.form.on('Job Card', { args: args }, freeze: true, - callback: function (r) { + callback: function () { frm.reload_doc(); frm.trigger("make_dashboard"); } - }) + }); }, update_sub_operation: function(frm, args) { diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index be7a8101734..046e2fd1825 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -16,15 +16,18 @@ "production_item", "item_name", "for_quantity", + "serial_no", "column_break_12", "wip_warehouse", "quality_inspection", "project", + "batch_no", "operation_section_section", "operation", "operation_row_number", "column_break_18", "workstation", + "employee", "section_break_21", "sub_operations", "timing_detail", @@ -163,8 +166,7 @@ "fieldname": "items", "fieldtype": "Table", "label": "Items", - "options": "Job Card Item", - "read_only": 1 + "options": "Job Card Item" }, { "collapsible": 1, @@ -373,11 +375,28 @@ "fieldtype": "Link", "label": "For Operation", "options": "Operation" + }, + { + "fieldname": "employee", + "fieldtype": "Table MultiSelect", + "label": "Employee", + "options": "Job Card Time Log" + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" } ], "is_submittable": 1, "links": [], - "modified": "2021-02-03 20:36:51.826944", + "modified": "2021-03-16 15:59:32.766484", "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 b4202e158d4..7f8f2ef68d0 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -4,9 +4,9 @@ from __future__ import unicode_literals import frappe -import datetime, json +import datetime +import json from frappe import _, bold -from six import string_types from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, @@ -33,11 +33,10 @@ class JobCard(Document): if self.operation: self.sub_operations = [] for row in frappe.get_all("Sub Operation", - filters = {"parent": self.operation}, fields=["operation"]): - self.append("sub_operations", { - "sub_operation": row.operation, - "status": "Pending" - }) + filters = {"parent": self.operation}, fields=["operation", "idx"]): + row.status = "Pending" + row.sub_operation = row.operation + self.append("sub_operations", row) def validate_time_logs(self): self.total_time_in_mins = 0.0 @@ -57,11 +56,14 @@ class JobCard(Document): d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 self.total_time_in_mins += d.time_in_mins - if d.completed_qty: + if d.completed_qty and not self.sub_operations: self.total_completed_qty += d.completed_qty self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) + for row in self.sub_operations: + self.total_completed_qty += row.completed_qty + def get_overlap_for(self, args, check_next_available_slot=False): production_capacity = 1 @@ -173,6 +175,10 @@ class JobCard(Document): def add_time_log(self, args): last_row = [] + employees = args.employees + if isinstance(employees, str): + employees = json.loads(employees) + if self.time_logs and len(self.time_logs) > 0: last_row = self.time_logs[-1] @@ -186,13 +192,7 @@ class JobCard(Document): "completed_qty": args.get("completed_qty") or 0.0 }) elif args.get("start_time"): - employees = args.employees - print(args) - if isinstance(employees, string_types): - employees = json.loads(employees) - for name in employees: - print(name.get('employee')) self.append("time_logs", { "from_time": get_datetime(args.get("start_time")), "employee": name.get('employee'), @@ -200,11 +200,21 @@ class JobCard(Document): "completed_qty": 0.0 }) + if not self.employee: + self.set_employees(employees) + if self.status == "On Hold": self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) self.save() + def set_employees(self, employees): + for name in employees: + self.append('employee', { + 'employee': name.get('employee'), + 'completed_qty': 0.0 + }) + def reset_timer_value(self, args): self.started_time = None @@ -221,24 +231,41 @@ class JobCard(Document): self.status = args.get("status") def update_sub_operation_status(self): - if not (self.sub_operations and self.time_logs): return + if not (self.sub_operations and self.time_logs): + return operation_wise_completed_time = {} for time_log in self.time_logs: if time_log.operation not in operation_wise_completed_time: operation_wise_completed_time.setdefault(time_log.operation, - frappe._dict({"status": "Pending", "completed_time": 0.0})) + frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []})) op_row = operation_wise_completed_time[time_log.operation] op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" + if self.status == 'On Hold': + op_row.status = 'Pause' + + op_row.employee.append(time_log.employee) if time_log.time_in_mins: op_row.completed_time += time_log.time_in_mins + op_row.completed_qty += time_log.completed_qty for row in self.sub_operations: operation_deatils = operation_wise_completed_time.get(row.sub_operation) if operation_deatils: - row.status = operation_deatils.status + if row.status != 'Complete': + row.status = operation_deatils.status + row.completed_time = operation_deatils.completed_time + if operation_deatils.employee: + row.completed_time = row.completed_time / len(set(operation_deatils.employee)) + + if operation_deatils.completed_qty: + row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee)) + else: + row.status = 'Pending' + row.completed_time = 0.0 + row.completed_qty = 0.0 def update_time_logs(self, row): self.append("time_logs", { @@ -275,6 +302,7 @@ class JobCard(Document): }) def on_submit(self): + self.validate_transfer_qty() self.validate_job_card() self.update_work_order() self.set_transferred_qty() @@ -283,7 +311,16 @@ class JobCard(Document): self.update_work_order() self.set_transferred_qty() + def validate_transfer_qty(self): + if self.items and self.transferred_qty < self.for_quantity: + frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}') + .format(self.name)) + def validate_job_card(self): + if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped': + frappe.throw(_("Transaction not allowed against stopped Work Order {0}") + .format(get_link_to_form('Work Order', self.work_order))) + if not self.time_logs: frappe.throw(_("Time logs are required for {0} {1}") .format(bold("Job Card"), get_link_to_form("Job Card", self.name))) @@ -299,6 +336,10 @@ class JobCard(Document): if not self.work_order: return + if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings', + 'add_corrective_operation_cost_in_finished_good_valuation')): + return + for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] @@ -346,8 +387,8 @@ class JobCard(Document): min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE - jctl.parent = jc.name and jc.work_order = %s - and jc.operation_id = %s and jc.docstatus = 1 + jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s + and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 """, (self.work_order, self.operation_id), as_dict=1) for data in wo.operations: @@ -453,9 +494,11 @@ class JobCard(Document): .format(bold(self.operation), work_order), OperationMismatchError) def validate_sequence_id(self): - if self.is_corrective_job_card: return + if self.is_corrective_job_card: + return - if not (self.work_order and self.sequence_id): return + if not (self.work_order and self.sequence_id): + return current_operation_qty = 0.0 data = self.get_current_operation_data() @@ -480,7 +523,7 @@ class JobCard(Document): @frappe.whitelist() def make_time_log(args): - if isinstance(args, string_types): + if isinstance(args, str): args = json.loads(args) args = frappe._dict(args) @@ -632,6 +675,8 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.for_operation = for_operation target.set('time_logs', []) + target.set('employee', []) + target.set('items', []) target.get_sub_operations() target.get_required_items() target.validate_time_logs() diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index a239a247e33..d91530dd3b5 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -17,8 +17,6 @@ "required_qty", "column_break_9", "transferred_qty", - "rate", - "amount", "allow_alternative_item" ], "fields": [ @@ -27,8 +25,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item", - "read_only": 1 + "options": "Item" }, { "fieldname": "source_warehouse", @@ -69,8 +66,7 @@ "fieldname": "required_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", - "read_only": 1 + "label": "Required Qty" }, { "fieldname": "column_break_9", @@ -102,25 +98,14 @@ "fieldtype": "Float", "label": "Transferred Qty", "no_copy": 1, - "print_hide": 1 - }, - { - "fieldname": "rate", - "fieldtype": "Currency", - "label": "Rate", - "read_only": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount", + "print_hide": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-11 13:50:13.804108", + "modified": "2021-04-22 18:50:00.003444", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Item", diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json index be8190236d1..9a8692b84d9 100644 --- a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json @@ -7,7 +7,8 @@ "field_order": [ "sub_operation", "completed_time", - "status" + "status", + "completed_qty" ], "fields": [ { @@ -34,12 +35,18 @@ "label": "Operation", "options": "Operation", "read_only": 1 + }, + { + "fieldname": "completed_qty", + "fieldtype": "Float", + "label": "Completed Qty", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-14 17:08:25.992957", + "modified": "2021-03-16 18:24:35.399593", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Operation", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 6647be54ebd..024f7847259 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -27,6 +27,7 @@ "overproduction_percentage_for_work_order", "other_settings_section", "update_bom_costs_automatically", + "add_corrective_operation_cost_in_finished_good_valuation", "column_break_23", "make_serial_no_batch_from_work_order" ], @@ -168,13 +169,19 @@ "fieldname": "make_serial_no_batch_from_work_order", "fieldtype": "Check", "label": "Make Serial No / Batch from Work Order" + }, + { + "default": "0", + "fieldname": "add_corrective_operation_cost_in_finished_good_valuation", + "fieldtype": "Check", + "label": "Add Corrective Operation Cost in Finished Good Valuation" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-08 13:37:40.325838", + "modified": "2021-03-16 15:54:38.967341", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js index 9bfcc6eedb6..102b6780e5f 100644 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ b/erpnext/manufacturing/doctype/operation/operation.js @@ -2,5 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Operation', { - + setup: function(frm) { + frm.set_query('operation', 'sub_operations', function() { + return { + filters: { + 'name': ['not in', [frm.doc.name]] + } + }; + }); + } }); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py index aaf0d5c01b4..374f32019bd 100644 --- a/erpnext/manufacturing/doctype/operation/operation.py +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt from frappe.model.document import Document class Operation(Document): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index acb3407e2bb..512048512ed 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -189,35 +189,41 @@ frappe.ui.form.on("Work Order", { const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), fields: [ { - fieldtype:'Link', - fieldname:'operation', + fieldtype: 'Link', + fieldname: 'operation', label: __('Operation'), - read_only:1, - in_list_view:1 + read_only: 1, + in_list_view: 1 }, { - fieldtype:'Link', - fieldname:'workstation', + fieldtype: 'Link', + fieldname: 'workstation', label: __('Workstation'), - read_only:1, - in_list_view:1 + read_only: 1, + in_list_view: 1 }, { - fieldtype:'Data', - fieldname:'name', + fieldtype: 'Data', + fieldname: 'name', label: __('Operation Id') }, { - fieldtype:'Float', - fieldname:'pending_qty', + fieldtype: 'Float', + fieldname: 'pending_qty', label: __('Pending Qty'), }, { - fieldtype:'Float', - fieldname:'qty', + fieldtype: 'Float', + fieldname: 'qty', label: __('Quantity to Manufacture'), - read_only:0, - in_list_view:1, + read_only: 0, + in_list_view: 1, + }, + { + fieldtype: 'Float', + fieldname: 'batch_size', + label: __('Batch Size'), + read_only: 1 }, ], data: operations_data, @@ -228,9 +234,13 @@ frappe.ui.form.on("Work Order", { }, function(data) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", + freeze: true, args: { work_order: frm.doc.name, operations: data.operations, + }, + callback: function() { + frm.reload_doc(); } }); }, __("Job Card"), __("Create")); @@ -247,6 +257,7 @@ frappe.ui.form.on("Work Order", { 'name': data.name, 'operation': data.operation, 'workstation': data.workstation, + 'batch_size': data.batch_size, 'qty': pending_qty, 'pending_qty': pending_qty }); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 8e99c665f1c..44d76d2b01c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -527,7 +527,8 @@ "depends_on": "has_serial_no", "fieldname": "serial_no", "fieldtype": "Small Text", - "label": "Serial Nos" + "label": "Serial Nos", + "no_copy": 1 }, { "default": "0", @@ -552,7 +553,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-03-16 13:27:51.116484", + "modified": "2021-06-20 15:19:14.902699", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c83f539e034..e343ed2dd38 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -27,9 +27,8 @@ class CapacityError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass -class SerialNoQtyError(frappe.ValidationError): pass - -from six import string_types +class SerialNoQtyError(frappe.ValidationError): + pass form_grid_templates = { "operations": "templates/form_grid/work_order_grid.html" @@ -248,7 +247,7 @@ class WorkOrder(Document): frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) - + if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): self.update_work_order_qty_in_combined_so() else: @@ -268,7 +267,7 @@ class WorkOrder(Document): self.update_work_order_qty_in_combined_so() else: self.update_work_order_qty_in_so() - + self.delete_job_card() self.update_completed_qty_in_material_request() self.update_planned_qty() @@ -277,10 +276,11 @@ class WorkOrder(Document): self.delete_auto_created_batch_and_serial_no() def create_serial_no_batch_no(self): - if not (self.has_serial_no or self.has_batch_no): return + if not (self.has_serial_no or self.has_batch_no): + return - if not cint(frappe.db.get_single_value("Manufacturing Settings", - "make_serial_no_batch_from_work_order")): return + if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + return if self.has_batch_no: self.create_batch_for_finished_good() @@ -346,29 +346,17 @@ class WorkOrder(Document): for index, row in enumerate(self.operations): qty = self.qty - i=0 while qty > 0: - i += 1 - if not cint(frappe.db.get_value("Operation", - row.operation, "create_job_card_based_on_batch_size")): - row.batch_size = self.qty - - job_card_qty = row.batch_size - if row.batch_size and qty >= row.batch_size: - qty -= row.batch_size - elif qty > 0: - job_card_qty = qty - qty = 0 - - if job_card_qty > 0: - self.prepare_data_for_job_card(row, job_card_qty, index, + qty = split_qty_based_on_batch_size(self, row, qty) + if row.job_card_qty > 0: + self.prepare_data_for_job_card(row, index, plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: self.db_set("planned_end_date", planned_end_date) - def prepare_data_for_job_card(self, row, job_card_qty, index, plan_days, enable_capacity_planning): + def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): self.set_operation_start_end_time(index, row) if not row.workstation: @@ -376,8 +364,8 @@ class WorkOrder(Document): .format(row.idx, row.operation)) original_start_time = row.planned_start_time - job_card_doc = create_job_card(self, row, qty=job_card_qty, - enable_capacity_planning=enable_capacity_planning, auto_create=True) + job_card_doc = create_job_card(self, row, auto_create=True, + enable_capacity_planning=enable_capacity_planning) if enable_capacity_planning and job_card_doc: row.planned_start_time = job_card_doc.time_logs[-1].from_time @@ -456,7 +444,7 @@ class WorkOrder(Document): work_order_qty = qty[0][0] if qty and qty[0][0] else 0 frappe.db.set_value('Sales Order Item', self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) - + def update_work_order_qty_in_combined_so(self): total_bundle_qty = 1 if self.product_bundle_item: @@ -469,7 +457,7 @@ class WorkOrder(Document): prod_plan = frappe.get_doc('Production Plan', self.production_plan) item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') - + for plan_reference in prod_plan.prod_plan_references: work_order_qty = 0.0 if plan_reference.item_reference == item_reference: @@ -477,7 +465,7 @@ class WorkOrder(Document): work_order_qty = flt(plan_reference.qty) / total_bundle_qty frappe.db.set_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty', work_order_qty) - + def update_completed_qty_in_material_request(self): if self.material_request: frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) @@ -761,8 +749,8 @@ class WorkOrder(Document): return bom def update_batch_produced_qty(self, stock_entry_doc): - if not cint(frappe.db.get_single_value("Manufacturing Settings", - "make_serial_no_batch_from_work_order")): return + if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + return for row in stock_entry_doc.items: if row.batch_no and (row.is_finished_item or row.is_scrap_item): @@ -848,7 +836,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): return wo_doc def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): - if isinstance(variant_items, string_types): + if isinstance(variant_items, str): variant_items = json.loads(variant_items) for item in variant_items: @@ -970,13 +958,47 @@ def query_sales_order(production_item): @frappe.whitelist() def make_job_card(work_order, operations): - if isinstance(operations, string_types): + if isinstance(operations, str): operations = json.loads(operations) work_order = frappe.get_doc('Work Order', work_order) for row in operations: + row = frappe._dict(row) validate_operation_data(row) - create_job_card(work_order, row, row.get("qty"), auto_create=True) + qty = row.get("qty") + while qty > 0: + qty = split_qty_based_on_batch_size(work_order, row, qty) + if row.job_card_qty > 0: + create_job_card(work_order, row, auto_create=True) + +def split_qty_based_on_batch_size(wo_doc, row, qty): + if not cint(frappe.db.get_value("Operation", + row.operation, "create_job_card_based_on_batch_size")): + row.batch_size = row.get("qty") or wo_doc.qty + + row.job_card_qty = row.batch_size + if row.batch_size and qty >= row.batch_size: + qty -= row.batch_size + elif qty > 0: + row.job_card_qty = qty + qty = 0 + + get_serial_nos_for_job_card(row, wo_doc) + + return qty + +def get_serial_nos_for_job_card(row, wo_doc): + if not wo_doc.serial_no: + return + + serial_nos = get_serial_nos(wo_doc.serial_no) + used_serial_nos = [] + for d in frappe.get_all('Job Card', fields=['serial_no'], + filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}): + used_serial_nos.extend(get_serial_nos(d.serial_no)) + + serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) + row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty]) def validate_operation_data(row): if row.get("qty") <= 0: @@ -995,21 +1017,22 @@ def validate_operation_data(row): ) ) -def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): +def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") doc.update({ 'work_order': work_order.name, 'operation': row.get("operation"), 'workstation': row.get("workstation"), 'posting_date': nowdate(), - 'for_quantity': qty or work_order.get('qty', 0), + 'for_quantity': row.job_card_qty or work_order.get('qty', 0), 'operation_id': row.get("name"), 'bom_no': work_order.bom_no, 'project': work_order.project, 'company': work_order.company, 'sequence_id': row.get("sequence_id"), 'wip_warehouse': work_order.wip_warehouse, - "hour_rate": row.get("hour_rate") + 'hour_rate': row.get("hour_rate"), + 'serial_no': row.get("serial_no") }) if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js index ef775663893..97e7e0a7d20 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -65,5 +65,41 @@ frappe.query_reports["Cost of Poor Quality Report"] = { fieldtype: "Link", options: "Workstation" }, + { + label: __("Item"), + fieldname: "production_item", + fieldtype: "Link", + options: "Item" + }, + { + label: __("Serial No"), + fieldname: "serial_no", + fieldtype: "Link", + options: "Serial No", + depends_on: "eval: doc.production_item", + get_query: function() { + var item_code = frappe.query_report.get_filter_value('production_item'); + return { + filters: { + item_code: item_code + } + } + } + }, + { + label: __("Batch No"), + fieldname: "batch_no", + fieldtype: "Link", + options: "Batch No", + depends_on: "eval: doc.production_item", + get_query: function() { + var item_code = frappe.query_report.get_filter_value('production_item'); + return { + filters: { + item: item_code + } + } + } + }, ] }; diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 2e8c191c608..9f81e7d26a1 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -20,7 +20,7 @@ def get_data(report_filters): if operations: operations = [d.name for d in operations] fields = ["production_item as item_code", "item_name", "work_order", "operation", - "workstation", "total_time_in_mins", "name", "hour_rate"] + "workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"] filters = get_filters(report_filters, operations) @@ -30,15 +30,18 @@ def get_data(report_filters): for row in job_cards: row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) update_raw_material_cost(row, report_filters) - update_time_details(row, report_filters, data) + data.append(row) return data def get_filters(report_filters, operations): filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} - for field in ["name", "work_order", "operation", "workstation", "company"]: + for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]: if report_filters.get(field): - filters[field] = report_filters.get(field) + if field != 'serial_no': + filters[field] = report_filters.get(field) + else: + filters[field] = ('like', '% {} %'.format(report_filters.get(field))) return filters @@ -48,24 +51,6 @@ def update_raw_material_cost(row, filters): filters={"parent": row.name, "docstatus": 1}): row.rm_cost += data.amount -def update_time_details(row, filters, data): - args = frappe._dict({"item_code": "", "item_name": "", "name": "", "work_order":"", - "operation": "", "workstation":"", "operating_cost": "", "rm_cost": "", "total_time_in_mins": ""}) - - i=0 - for time_log in frappe.get_all("Job Card Time Log", - fields = ["from_time", "to_time", "time_in_mins"], - filters={"parent": row.name, "docstatus": 1, - "from_time": (">=", filters.from_date), "to_time": ("<=", filters.to_date)}): - - if i==0: - i += 1 - row.update(time_log) - data.append(row) - else: - args.update(time_log) - data.append(args) - def get_columns(filters): return [ { @@ -102,6 +87,18 @@ def get_columns(filters): "options": "Operation", "width": "100" }, + { + "label": _("Serial No"), + "fieldtype": "Data", + "fieldname": "serial_no", + "width": "100" + }, + { + "label": _("Batch No"), + "fieldtype": "Data", + "fieldname": "batch_no", + "width": "100" + }, { "label": _("Workstation"), "fieldtype": "Link", @@ -126,23 +123,5 @@ def get_columns(filters): "fieldtype": "Float", "fieldname": "total_time_in_mins", "width": "100" - }, - { - "label": _("From Time"), - "fieldtype": "Datetime", - "fieldname": "from_time", - "width": "100" - }, - { - "label": _("To Time"), - "fieldtype": "Datetime", - "fieldname": "to_time", - "width": "100" - }, - { - "label": _("Time in Mins"), - "fieldtype": "Float", - "fieldname": "time_in_mins", - "width": "100" - }, + } ] \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e49c9a57c3e..8f27ef4356c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1097,7 +1097,8 @@ class StockEntry(StockController): def set_batchwise_finished_goods(self, args, item): qty = flt(self.fg_completed_qty) - filters = {"reference_name": self.pro_doc.name, + filters = { + "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, "qty_to_produce": (">", 0) } @@ -1106,7 +1107,8 @@ class StockEntry(StockController): for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): batch_qty = flt(row.qty) - flt(row.produced_qty) - if not batch_qty: continue + if not batch_qty: + continue if qty <=0: break @@ -1701,6 +1703,10 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if bom.quantity: operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) + if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings', + 'add_corrective_operation_cost_in_finished_good_valuation')): + operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) + return operating_cost_per_unit def get_used_alternative_items(purchase_order=None, work_order=None): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 864ff488b22..a1782839048 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -18,6 +18,7 @@ "col_break2", "is_finished_item", "is_scrap_item", + "quality_inspection", "subcontracted_item", "section_break_8", "description", @@ -69,7 +70,6 @@ "putaway_rule", "column_break_51", "reference_purchase_receipt", - "quality_inspection", "job_card_item" ], "fields": [ @@ -548,7 +548,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-11 13:47:50.158754", + "modified": "2021-04-22 20:08:23.799715", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From a50196bcd93c949fe12e8b59603d2a47aa104b42 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Thu, 24 Jun 2021 18:25:16 +0530 Subject: [PATCH 247/376] fix: dunning permission error in payment entry (#26204) --- erpnext/accounts/doctype/dunning/dunning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 1a6dbedf560..7c1a1717da2 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -86,7 +86,7 @@ def resolve_dunning(doc, state): for reference in doc.references: if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: dunnings = frappe.get_list('Dunning', filters={ - 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}) + 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True) for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') From 9382b1f154502cdfc50be75042bc270d7aae3eb8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:03:22 +0530 Subject: [PATCH 248/376] fix: Flaky test --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ff433b962ff..2f5d36c8fab 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -966,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase): update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) # Create Purchase Order with TDS applied - po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000) + po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item') po.apply_tds = 1 po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() @@ -1002,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Invoice against Purchase Order purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice.allocate_advances_automatically = 1 + purchase_invoice.items[0].item_code = '_Test Non Stock Item' purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' purchase_invoice.save() purchase_invoice.submit() From 7a63d782db4bcd816f13d277c7fb2c2a051b3392 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 24 Jun 2021 19:07:16 +0530 Subject: [PATCH 249/376] fix: batch nos in packed items (#26105) * test: batch info in packed_items * fix(ux): make packed items editable * refactor: allow custom table name for set_batch In some doctypes there are multiple child tables requiring batched items. This change makes the function a bit more flexible. * fix: Auto fetch batch_nos in packed_item table --- erpnext/stock/doctype/batch/batch.py | 4 +-- .../doctype/delivery_note/delivery_note.js | 3 ++ .../doctype/delivery_note/delivery_note.json | 5 ++-- .../doctype/delivery_note/delivery_note.py | 7 +++-- .../delivery_note/test_delivery_note.py | 29 +++++++++++++++---- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 508e17c3409..83e10a0d400 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -226,9 +226,9 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): return batch.name -def set_batch_nos(doc, warehouse_field, throw=False): +def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" - for d in doc.items: + for d in doc.get(child_table): qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no') warehouse = d.get(warehouse_field, None) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index c3803f19a10..36dfa6d7951 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -78,6 +78,9 @@ frappe.ui.form.on("Delivery Note", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + + frm.set_df_property('packed_items', 'cannot_add_rows', true); + frm.set_df_property('packed_items', 'cannot_delete_rows', true); }, print_without_amount: function(frm) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 280fde158f5..f20e76f5bf6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -554,8 +554,7 @@ "oldfieldname": "packing_details", "oldfieldtype": "Table", "options": "Packed Item", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "product_bundle_help", @@ -1289,7 +1288,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-04-15 23:55:49.620641", + "modified": "2021-06-11 19:27:30.901112", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index dd31965face..fcdb5f3b190 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -129,12 +129,13 @@ class DeliveryNote(SellingController): self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() - if self._action != 'submit' and not self.is_return: - set_batch_nos(self, 'warehouse', True) - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list make_packing_list(self) + if self._action != 'submit' and not self.is_return: + set_batch_nos(self, 'warehouse', throw=True) + set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items") + self.update_current_stock() if not self.installation_status: self.installation_status = 'Not Installed' diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 0c63df0e225..f981aeb13bb 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -7,7 +7,7 @@ import unittest import frappe import json import frappe.defaults -from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today +from frappe.utils import nowdate, nowtime, cstr, flt from erpnext.stock.stock_ledger import get_previous_sle from erpnext.accounts.utils import get_balance_on from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries @@ -18,9 +18,11 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWa from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \ import create_stock_reconciliation, set_valuation_method from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so -from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account +from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + class TestDeliveryNote(unittest.TestCase): def test_over_billing_against_dn(self): @@ -277,8 +279,6 @@ class TestDeliveryNote(unittest.TestCase): dn.cancel() def test_sales_return_for_non_bundled_items_full(self): - from erpnext.stock.doctype.item.test_item import make_item - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') make_item("Box", {'is_stock_item': 1}) @@ -741,6 +741,25 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(si2.items[0].qty, 2) self.assertEqual(si2.items[1].qty, 1) + + def test_delivery_note_bundle_with_batched_item(self): + batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) + batched_item = make_item("_Test Batched Item", + {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"} + ) + make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) + make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42) + + try: + dn = create_delivery_note(item_code=batched_bundle.name, qty=1) + except frappe.ValidationError as e: + if "batch" in str(e).lower(): + self.fail("Batch numbers not getting added to bundled items in DN.") + raise e + + self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) From e21e435a0d5b2aa6c6433233d499e09b63139f03 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:17:58 +0530 Subject: [PATCH 250/376] fix: Add python 3 compatible string types --- erpnext/public/js/controllers/taxes_and_totals.js | 4 ++-- erpnext/stock/get_item_details.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 4a14a665cda..3f76a3e9275 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -288,8 +288,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ company: me.frm.doc.company, tax_category: cstr(me.frm.doc.tax_category), item_codes: item_codes, - item_tax_templates: item_tax_templates, - item_rates: item_rates + item_rates: item_rates, + item_tax_templates: item_tax_templates }, callback: function(r) { if (!r.exc) { diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 37850350abb..c64084fe340 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,15 +436,15 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if isinstance(item_codes, string_types): + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) - if isinstance(item_rates, string_types): + if isinstance(item_rates, (str,)): item_rates = json.loads(item_rates) - if isinstance(item_tax_templates, string_types): + if isinstance(item_tax_templates, (str,)): item_tax_templates = json.loads(item_tax_templates) for item_code in item_codes: @@ -514,7 +514,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return None # do not change if already a valid template - if args.get('item_tax_template') in [t.item_tax_template for t in taxes]: + if args.get('item_tax_template') in {t.item_tax_template for t in taxes}: out["item_tax_template"] = args.get('item_tax_template') return args.get('item_tax_template') From 1658107a926ee1880a79581158e0dd8205ae5f9f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:18:50 +0530 Subject: [PATCH 251/376] fix: Linting fixes --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 3f76a3e9275..1de9ec1a7df 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -277,7 +277,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ // Use combination of name and item code in case same item is added multiple times item_codes.push([item.item_code, item.name]); item_rates[item.name] = item.net_rate; - item_tax_templates[item.name] = item.item_tax_template + item_tax_templates[item.name] = item.item_tax_template; } }); From 64c6b34b9493d0f5f2be54061e7e3f39a23551ca Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Jun 2021 19:29:56 +0530 Subject: [PATCH 252/376] fix: too many writes while renaming company abbreviation (#26142) --- erpnext/setup/doctype/company/company.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 27e023c1e5b..0427abe558d 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -407,8 +407,6 @@ def replace_abbr(company, old, new): frappe.only_for("System Manager") - frappe.db.set_value("Company", company, "abbr", new) - def _rename_record(doc): parts = doc[0].rsplit(" - ", 1) if len(parts) == 1 or parts[1].lower() == old.lower(): @@ -419,11 +417,18 @@ def replace_abbr(company, old, new): doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company)) for d in doc: _rename_record(d) + try: + frappe.db.auto_commit_on_many_writes = 1 + frappe.db.set_value("Company", company, "abbr", new) + for dt in ["Warehouse", "Account", "Cost Center", "Department", + "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: + _rename_records(dt) + frappe.db.commit() - for dt in ["Warehouse", "Account", "Cost Center", "Department", - "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: - _rename_records(dt) - frappe.db.commit() + except Exception: + frappe.log_error(title=_('Abbreviation Rename Error')) + finally: + frappe.db.auto_commit_on_many_writes = 0 def get_name_with_abbr(name, company): From b3a0a7b4329aa1574a6d42abf61bf8691b8f8145 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Jun 2021 19:30:10 +0530 Subject: [PATCH 253/376] fix: too many writes while renaming company abbreviation (#26203) --- erpnext/setup/doctype/company/company.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 27e023c1e5b..0427abe558d 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -407,8 +407,6 @@ def replace_abbr(company, old, new): frappe.only_for("System Manager") - frappe.db.set_value("Company", company, "abbr", new) - def _rename_record(doc): parts = doc[0].rsplit(" - ", 1) if len(parts) == 1 or parts[1].lower() == old.lower(): @@ -419,11 +417,18 @@ def replace_abbr(company, old, new): doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company)) for d in doc: _rename_record(d) + try: + frappe.db.auto_commit_on_many_writes = 1 + frappe.db.set_value("Company", company, "abbr", new) + for dt in ["Warehouse", "Account", "Cost Center", "Department", + "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: + _rename_records(dt) + frappe.db.commit() - for dt in ["Warehouse", "Account", "Cost Center", "Department", - "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: - _rename_records(dt) - frappe.db.commit() + except Exception: + frappe.log_error(title=_('Abbreviation Rename Error')) + finally: + frappe.db.auto_commit_on_many_writes = 0 def get_name_with_abbr(name, company): From ac0bc1f1707505d847aff6eee1da7c6b69794567 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 24 Jun 2021 19:36:17 +0530 Subject: [PATCH 254/376] fix: precision rate for packed items (#26208) --- erpnext/controllers/selling_controller.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7f28289760c..da2765deded 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -330,9 +330,15 @@ class SellingController(StockController): # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): - rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) - if d.rate != rate: - d.rate = rate + if d.doctype == "Packed Item": + incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate')) + if d.incoming_rate != incoming_rate: + d.incoming_rate = incoming_rate + else: + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 d.discount_amount = 0 frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") From 5708b7140b45db45d3239f9cf1407cdaf89b6eae Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 24 Jun 2021 19:38:37 +0530 Subject: [PATCH 255/376] fix: batch nos in packed items (bp #26105) * test: batch info in packed_items * fix(ux): make packed items editable * refactor: allow custom table name for set_batch In some doctypes there are multiple child tables requiring batched items. This change makes the function a bit more flexible. * fix: Auto fetch batch_nos in packed_item table --- erpnext/stock/doctype/batch/batch.py | 4 +-- .../doctype/delivery_note/delivery_note.js | 3 ++ .../doctype/delivery_note/delivery_note.json | 5 ++-- .../doctype/delivery_note/delivery_note.py | 7 +++-- .../delivery_note/test_delivery_note.py | 29 +++++++++++++++---- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index bb5ad5c6fee..cd441b5958d 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -226,9 +226,9 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): return batch.name -def set_batch_nos(doc, warehouse_field, throw=False): +def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" - for d in doc.items: + for d in doc.get(child_table): qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no') warehouse = d.get(warehouse_field, None) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 7875b9cd87f..74cb3fcb1f0 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -78,6 +78,9 @@ frappe.ui.form.on("Delivery Note", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + + frm.set_df_property('packed_items', 'cannot_add_rows', true); + frm.set_df_property('packed_items', 'cannot_delete_rows', true); }, print_without_amount: function(frm) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 280fde158f5..f20e76f5bf6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -554,8 +554,7 @@ "oldfieldname": "packing_details", "oldfieldtype": "Table", "options": "Packed Item", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "product_bundle_help", @@ -1289,7 +1288,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-04-15 23:55:49.620641", + "modified": "2021-06-11 19:27:30.901112", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index dd31965face..fcdb5f3b190 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -129,12 +129,13 @@ class DeliveryNote(SellingController): self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() - if self._action != 'submit' and not self.is_return: - set_batch_nos(self, 'warehouse', True) - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list make_packing_list(self) + if self._action != 'submit' and not self.is_return: + set_batch_nos(self, 'warehouse', throw=True) + set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items") + self.update_current_stock() if not self.installation_status: self.installation_status = 'Not Installed' diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 0c63df0e225..f981aeb13bb 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -7,7 +7,7 @@ import unittest import frappe import json import frappe.defaults -from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today +from frappe.utils import nowdate, nowtime, cstr, flt from erpnext.stock.stock_ledger import get_previous_sle from erpnext.accounts.utils import get_balance_on from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries @@ -18,9 +18,11 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWa from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \ import create_stock_reconciliation, set_valuation_method from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so -from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account +from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + class TestDeliveryNote(unittest.TestCase): def test_over_billing_against_dn(self): @@ -277,8 +279,6 @@ class TestDeliveryNote(unittest.TestCase): dn.cancel() def test_sales_return_for_non_bundled_items_full(self): - from erpnext.stock.doctype.item.test_item import make_item - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') make_item("Box", {'is_stock_item': 1}) @@ -741,6 +741,25 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(si2.items[0].qty, 2) self.assertEqual(si2.items[1].qty, 1) + + def test_delivery_note_bundle_with_batched_item(self): + batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) + batched_item = make_item("_Test Batched Item", + {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"} + ) + make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) + make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42) + + try: + dn = create_delivery_note(item_code=batched_bundle.name, qty=1) + except frappe.ValidationError as e: + if "batch" in str(e).lower(): + self.fail("Batch numbers not getting added to bundled items in DN.") + raise e + + self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) From bdba29bab56491eefb26281a804974739d240e85 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:48:11 +0530 Subject: [PATCH 256/376] fix: Account filter not working with accounting dimension filter --- .../doctype/accounting_dimension/accounting_dimension.py | 2 +- erpnext/accounts/report/general_ledger/general_ledger.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 7cd1e7736c8..fac28c92397 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -19,7 +19,7 @@ class AccountingDimension(Document): def validate(self): if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', - 'Cost Center', 'Accounting Dimension Detail', 'Company') : + 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') : msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) frappe.throw(msg) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 03808c3640c..914058e6339 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -211,7 +211,7 @@ def get_gl_entries(filters, accounting_dimensions): dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, order_by_statement=order_by_statement ), - filters, as_dict=1) + filters, as_dict=1, debug=1) if filters.get('presentation_currency'): return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) @@ -222,7 +222,7 @@ def get_gl_entries(filters, accounting_dimensions): def get_conditions(filters): conditions = [] - if filters.get("account") and not filters.get("include_dimensions"): + if filters.get("account"): filters.account = get_accounts_with_children(filters.account) conditions.append("account in %(account)s") From e3ca1778281c3e6d409741d39f641ea2f7cbbb16 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:51:23 +0530 Subject: [PATCH 257/376] fix: Remove debug flag --- erpnext/accounts/report/general_ledger/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 914058e6339..744ada9e558 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -211,7 +211,7 @@ def get_gl_entries(filters, accounting_dimensions): dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, order_by_statement=order_by_statement ), - filters, as_dict=1, debug=1) + filters, as_dict=1) if filters.get('presentation_currency'): return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) From 0ceec76d984c5eca8f2f9e3d92683bb8c3866eba Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 24 Jun 2021 20:27:47 +0530 Subject: [PATCH 258/376] refactor: update cost updates operation time and hour rates in BOM (fp #25891) * refactor: updates hour_rate and operation time on update cost * refactor: hour_rates are updated in routing when updated in workstations * test: test cases for updating hour_rates and operation time in linked bom --- erpnext/manufacturing/doctype/bom/bom.py | 45 +++++++++----- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../manufacturing/doctype/routing/routing.py | 14 ++++- .../doctype/routing/test_routing.py | 58 +++++++++++++++-- .../doctype/workstation/test_workstation.py | 62 ++++++++++++++++++- .../doctype/workstation/workstation.py | 5 +- 6 files changed, 157 insertions(+), 29 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d1f63854c71..3f109d91b5e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -81,7 +81,7 @@ class BOM(WebsiteGenerator): self.validate_operations() self.calculate_cost() self.update_stock_qty() - self.update_cost(update_parent=False, from_child_bom=True, save=False) + self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -213,7 +213,7 @@ class BOM(WebsiteGenerator): return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) @frappe.whitelist() - def update_cost(self, update_parent=True, from_child_bom=False, save=True): + def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True): if self.docstatus == 2: return @@ -242,7 +242,7 @@ class BOM(WebsiteGenerator): if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True - self.calculate_cost() + self.calculate_cost(update_hour_rate) if save: self.db_update() @@ -403,32 +403,47 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self): + def calculate_cost(self, update_hour_rate = False): """Calculate bom totals""" - self.calculate_op_cost() + self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost() self.calculate_sm_cost() self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost - def calculate_op_cost(self): + def calculate_op_cost(self, update_hour_rate = False): """Update workstation rate and calculates totals""" self.operating_cost = 0 self.base_operating_cost = 0 for d in self.get('operations'): if d.workstation: - if not d.hour_rate: - hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate")) - d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate - - if d.hour_rate and d.time_in_mins: - d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate) - d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 - d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate) + self.update_rate_and_time(d, update_hour_rate) self.operating_cost += flt(d.operating_cost) self.base_operating_cost += flt(d.base_operating_cost) + def update_rate_and_time(self, row, update_hour_rate = False): + if not row.hour_rate or update_hour_rate: + hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) + row.hour_rate = (hour_rate / flt(self.conversion_rate) + if self.conversion_rate and hour_rate else hour_rate) + + if self.routing: + row.time_in_mins = flt(frappe.db.get_value("BOM Operation", { + "workstation": row.workstation, + "operation": row.operation, + "sequence_id": row.sequence_id, + "parent": self.routing + }, ["time_in_mins"])) + + if row.hour_rate and row.time_in_mins: + row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + + if update_hour_rate: + row.db_update() + def calculate_rm_cost(self): """Fetch RM rate as per today's valuation rate and calculate totals""" total_rm_cost = 0 @@ -975,7 +990,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - + return frappe.get_all("Item", fields = fields, filters=query_filters, or_filters = or_cond_filters, order_by=order_by, diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 42b23f223d2..1f443fb95ae 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -123,7 +123,7 @@ class TestBOM(unittest.TestCase): bom.items[0].conversion_factor = 5 bom.insert() - bom.update_cost() + bom.update_cost(update_hour_rate = False) # test amounts in selected currency self.assertEqual(bom.items[0].rate, 300) diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index 8312d7436c2..ece0db717a2 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -4,14 +4,24 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint +from frappe.utils import cint, flt from frappe import _ from frappe.model.document import Document class Routing(Document): def validate(self): + self.calculate_operating_cost() self.set_routing_id() + def on_update(self): + self.calculate_operating_cost() + + def calculate_operating_cost(self): + for operation in self.operations: + if not operation.hour_rate: + operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') + operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2) + def set_routing_id(self): sequence_id = 0 for row in self.operations: @@ -21,4 +31,4 @@ class Routing(Document): frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") .format(row.idx, row.sequence_id, sequence_id)) - sequence_id = row.sequence_id \ No newline at end of file + sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 6a38dcfa030..92f26946ab7 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -7,9 +7,7 @@ import unittest import frappe from frappe.test_runner import make_test_records from erpnext.stock.doctype.item.test_item import make_item -from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError -from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record class TestRouting(unittest.TestCase): @@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase): wo_doc.cancel() wo_doc.delete() + def test_update_bom_operation_time(self): + operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "hour_rate_labour": 750 , + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation B", + "hour_rate_labour": 200, + "hour_rate_rent": 1000, + "time_in_mins": 20 + } + ] + + test_routing_operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "time_in_mins": 20 + } + ] + setup_operations(operations) + routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR') + self.assertEqual(routing_doc.operations[0].time_in_mins, 30) + self.assertEqual(routing_doc.operations[1].time_in_mins, 20) + routing_doc.operations[0].time_in_mins = 90 + routing_doc.operations[1].time_in_mins = 42.2 + routing_doc.save() + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(bom_doc.operations[0].time_in_mins, 90) + self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2) + + def setup_operations(rows): + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + from erpnext.manufacturing.doctype.operation.test_operation import make_operation for row in rows: make_workstation(row) make_operation(row) @@ -61,12 +105,14 @@ def create_routing(**args): if not args.do_not_save: try: - for operation in args.operations: - doc.append("operations", operation) - doc.insert() except frappe.DuplicateEntryError: doc = frappe.get_doc("Routing", args.routing_name) + doc.delete_key('operations') + for operation in args.operations: + doc.append("operations", operation) + + doc.save() return doc @@ -91,7 +137,7 @@ def setup_bom(**args): name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name') if not name: bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"), - routing = args.routing, with_operations=1) + routing = args.routing, with_operations=1, currency = args.currency) else: bom_doc = frappe.get_doc("BOM", name) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index c6699bee48c..9b73aca6010 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -1,16 +1,19 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt from __future__ import unicode_literals +from erpnext.manufacturing.doctype.operation.test_operation import make_operation import frappe import unittest from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError +from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing +from frappe.test_runner import make_test_records test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') +make_test_records('Workstation') class TestWorkstation(unittest.TestCase): - def test_validate_timings(self): check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") @@ -21,6 +24,58 @@ class TestWorkstation(unittest.TestCase): self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + def test_update_bom_operation_rate(self): + operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "time_in_mins": 60 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation B", + "hour_rate_rent": 1000, + "time_in_mins": 60 + } + ] + + for row in operations: + make_workstation(row) + make_operation(row) + + test_routing_operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "time_in_mins": 60 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "time_in_mins": 60 + } + ] + routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations) + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") + w1 = frappe.get_doc("Workstation", "_Test Workstation A") + #resets values + w1.hour_rate_rent = 300 + w1.hour_rate_labour = 0 + w1.save() + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(w1.hour_rate, 300) + self.assertEqual(bom_doc.operations[0].hour_rate, 300) + w1.hour_rate_rent = 250 + w1.save() + #updating after setting new rates in workstations + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(w1.hour_rate, 250) + self.assertEqual(bom_doc.operations[0].hour_rate, 250) + self.assertEqual(bom_doc.operations[1].hour_rate, 250) + def make_workstation(*args, **kwargs): args = args if args else kwargs if isinstance(args, tuple): @@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs): "doctype": "Workstation", "workstation_name": workstation_name }) - + doc.hour_rate_rent = args.get("hour_rate_rent") + doc.hour_rate_labour = args.get("hour_rate_labour") doc.insert() return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Workstation", workstation_name) \ No newline at end of file + return frappe.get_doc("Workstation", workstation_name) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 3512e590458..f4483f75472 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -39,7 +39,8 @@ class Workstation(Document): def update_bom_operation(self): bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` - where workstation = %s""", self.name) + where workstation = %s and parenttype = 'routing' """, self.name) + for bom_no in bom_list: frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s where parent = %s and workstation = %s""", @@ -71,7 +72,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da def is_within_operating_hours(workstation, operation, from_datetime, to_datetime): operation_length = time_diff_in_seconds(to_datetime, from_datetime) workstation = frappe.get_doc("Workstation", workstation) - + if not workstation.working_hours: return From 2ea750eaddce38d34a682f7c0884f939021d48b2 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 24 Jun 2021 20:29:18 +0530 Subject: [PATCH 259/376] perf: don't query unless required (#26175) re-order conditionals so queries are not evaluated unless required. --- erpnext/controllers/sales_and_purchase_return.py | 9 +++++---- erpnext/stock/doctype/batch/batch.py | 5 ++--- erpnext/stock/doctype/delivery_note/delivery_note.py | 5 ++--- .../stock/doctype/material_request/material_request.py | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5f759b43bc6..80ccc6d75b2 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -99,9 +99,10 @@ def validate_returned_items(doc): frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") .format(d.idx, s, doc.doctype, doc.return_against)) - if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \ - and not d.get("warehouse"): - frappe.throw(_("Warehouse is mandatory")) + if (warehouse_mandatory and not d.get("warehouse") and + frappe.db.get_value("Item", d.item_code, "is_stock_item") + ): + frappe.throw(_("Warehouse is mandatory")) items_returned = True @@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc): for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): serial_nos.extend(get_serial_nos(row.serial_no)) - return serial_nos \ No newline at end of file + return serial_nos diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 83e10a0d400..30bdbe8db99 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -230,9 +230,8 @@ def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" for d in doc.get(child_table): qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 - has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no') warehouse = d.get(warehouse_field, None) - if has_batch_no and warehouse and qty > 0: + if warehouse and qty > 0 and frappe.db.get_value('Item', d.item_code, 'has_batch_no'): if not d.batch_no: d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) else: @@ -308,4 +307,4 @@ def validate_serial_no_with_batch(serial_nos, item_code): message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" frappe.throw(_("There is no batch found against the {0}: {1}") - .format(message, serial_no_link)) \ No newline at end of file + .format(message, serial_no_link)) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index fcdb5f3b190..4808e948fc6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -182,9 +182,8 @@ class DeliveryNote(SellingController): super(DeliveryNote, self).validate_warehouse() for d in self.get_item_list(): - if frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: - if not d['warehouse']: - frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) + if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: + frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) def update_current_stock(self): diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 335175f21d1..3ad9909ad07 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -189,7 +189,7 @@ class MaterialRequest(BuyingController): item_wh_list = [] for d in self.get("items"): if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \ - and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and d.warehouse: + and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 : item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: From c151c67d979939d5ba083cd0dae2e6529111bbc5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:03:22 +0530 Subject: [PATCH 260/376] fix: Flaky test --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ff433b962ff..2f5d36c8fab 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -966,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase): update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) # Create Purchase Order with TDS applied - po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000) + po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item') po.apply_tds = 1 po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() @@ -1002,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Invoice against Purchase Order purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice.allocate_advances_automatically = 1 + purchase_invoice.items[0].item_code = '_Test Non Stock Item' purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' purchase_invoice.save() purchase_invoice.submit() From 8cdd7ce7b645b5775e7d098700e0d0d5734308ee Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:17:58 +0530 Subject: [PATCH 261/376] fix: Add python 3 compatible string types --- erpnext/public/js/controllers/taxes_and_totals.js | 4 ++-- erpnext/stock/get_item_details.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index a25429f76df..9a5f81c7b81 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -290,8 +290,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { company: me.frm.doc.company, tax_category: cstr(me.frm.doc.tax_category), item_codes: item_codes, - item_tax_templates: item_tax_templates, - item_rates: item_rates + item_rates: item_rates, + item_tax_templates: item_tax_templates }, callback: function(r) { if (!r.exc) { diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 37850350abb..c64084fe340 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,15 +436,15 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if isinstance(item_codes, string_types): + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) - if isinstance(item_rates, string_types): + if isinstance(item_rates, (str,)): item_rates = json.loads(item_rates) - if isinstance(item_tax_templates, string_types): + if isinstance(item_tax_templates, (str,)): item_tax_templates = json.loads(item_tax_templates) for item_code in item_codes: @@ -514,7 +514,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return None # do not change if already a valid template - if args.get('item_tax_template') in [t.item_tax_template for t in taxes]: + if args.get('item_tax_template') in {t.item_tax_template for t in taxes}: out["item_tax_template"] = args.get('item_tax_template') return args.get('item_tax_template') From 7b7796c1ed2774a6de9de09e26c5f889f7dbf7e5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:18:50 +0530 Subject: [PATCH 262/376] fix: Linting fixes --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 9a5f81c7b81..0471704c015 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -279,7 +279,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Use combination of name and item code in case same item is added multiple times item_codes.push([item.item_code, item.name]); item_rates[item.name] = item.net_rate; - item_tax_templates[item.name] = item.item_tax_template + item_tax_templates[item.name] = item.item_tax_template; } }); From f4e8e7d933fc7d96d22da7e23fe22f9bb6cba200 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:18:59 +0530 Subject: [PATCH 263/376] fix(Asset): Remove extra tabs --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 29379657a10..95e3c8ad715 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -196,7 +196,7 @@ class Asset(AccountsController): if has_pro_rata: number_of_pending_depreciations += 1 - + skip_row = False for n in range(start, number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) From 289b449f2bf11e79fef5501bc5e012f0e589e416 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:21:08 +0530 Subject: [PATCH 264/376] fix(Asset): Add comment --- erpnext/assets/doctype/asset/asset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 95e3c8ad715..66f0bdcd588 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -224,6 +224,7 @@ class Asset(AccountsController): # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: if not self.flags.increase_in_asset_life: + # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, n * cint(d.frequency_of_depreciation)) From b44e4121da48debd9b0481d558625afca781e150 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:25:45 +0530 Subject: [PATCH 265/376] fix(Asset Repair): Move filters for cost_center, warehouse and project to setup --- .../doctype/asset_repair/asset_repair.js | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 91bed4fdd03..1cebfff66e5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,6 +2,34 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { + setup: function(frm) { + frm.fields_dict.cost_center.get_query = function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }; + + frm.fields_dict.project.get_query = function(doc) { + return { + filters: { + 'company': doc.company + } + }; + }; + + frm.fields_dict.warehouse.get_query = function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }; + }, + refresh: function(frm) { if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { @@ -40,30 +68,4 @@ frappe.ui.form.on('Asset Repair Consumed Item', { var row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); }, -}); - -cur_frm.fields_dict.cost_center.get_query = function(doc) { - return { - filters: { - 'is_group': 0, - 'company': doc.company - } - }; -}; - -cur_frm.fields_dict.project.get_query = function(doc) { - return { - filters: { - 'company': doc.company - } - }; -}; - -cur_frm.fields_dict.warehouse.get_query = function(doc) { - return { - filters: { - 'is_group': 0, - 'company': doc.company - } - }; -}; \ No newline at end of file +}); \ No newline at end of file From c73e137bfa934de13b2512a44243ba810f123553 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:27:30 +0530 Subject: [PATCH 266/376] fix(Asset Repair): Edit description for total_repair_cost --- erpnext/assets/doctype/asset_repair/asset_repair.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 6a14384f3ff..0651deb2acc 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -212,7 +212,7 @@ }, { "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0", - "description": "Sum of Repair Cost and the total value of all Stock Items consumed during the repair.", + "description": "Sum of Repair Cost and Value of Consumed Stock Items.", "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost", From dcb8a2895cee137054656487005706007bf6691f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:30:08 +0530 Subject: [PATCH 267/376] fix(Asset Repair): Simplify code for Asset Repair creation in tests --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index b3d78b3bfbb..30bbb37851e 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -138,10 +138,7 @@ def create_asset_repair(**args): "consumed_quantity": args.qty or 1 }) - try: - asset_repair.save() - except frappe.DuplicateEntryError: - pass + asset_repair.insert(ignore_if_duplicate=True) if args.submit: asset_repair.repair_status = "Completed" From 37a25308ae03e1134a263036bd736155a8af4aa4 Mon Sep 17 00:00:00 2001 From: Alan <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 12:40:38 +0530 Subject: [PATCH 268/376] fix: add validation for 'for_qty' else throws errors (#25829) (#26214) * fix: add validation for 'for_qty' else throws errors * fix: check if for_qty is None * fix: check purpose * fix: add purpose to pick list get_doc * fix: set as read only to prevent se from picking up value * chore: undo changes to doctype modified timestamp --- erpnext/stock/doctype/pick_list/pick_list.json | 2 +- erpnext/stock/doctype/pick_list/pick_list.py | 9 +++++++++ erpnext/stock/doctype/pick_list/test_pick_list.py | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index c01388dcd21..21467935370 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -184,4 +184,4 @@ "sort_field": "modified", "sort_order": "DESC", "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 6ab68e292ae..e795742ea4d 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -17,6 +17,9 @@ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note a # TODO: Prioritize SO or WO group warehouse class PickList(Document): + def validate(self): + self.validate_for_qty() + def before_save(self): self.set_item_locations() @@ -35,6 +38,7 @@ class PickList(Document): @frappe.whitelist() def set_item_locations(self, save=False): + self.validate_for_qty() items = self.aggregate_item_qty() self.item_location_map = frappe._dict() @@ -107,6 +111,11 @@ class PickList(Document): return item_map.values() + def validate_for_qty(self): + if self.purpose == "Material Transfer for Manufacture" \ + and (self.for_qty is None or self.for_qty == 0): + frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) + def validate_item_locations(pick_list): if not pick_list.locations: diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index c4da05a6d44..84566b8d8c7 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -37,6 +37,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item', 'qty': 5, @@ -90,6 +91,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item Warehouse Group Wise Reorder', 'qty': 1000, @@ -135,6 +137,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Serialized Item', 'qty': 1000, @@ -264,6 +267,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item', 'qty': 5, @@ -319,6 +323,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item', 'qty': 1, From 532a224c4456f0fe8bb9805d76d4211d2af79613 Mon Sep 17 00:00:00 2001 From: Ankush Date: Fri, 25 Jun 2021 13:28:01 +0530 Subject: [PATCH 269/376] fix: precision rate for packed items (#26046) (#26217) Co-authored-by: Noah Jacob --- erpnext/controllers/selling_controller.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7f28289760c..da2765deded 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -330,9 +330,15 @@ class SellingController(StockController): # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): - rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) - if d.rate != rate: - d.rate = rate + if d.doctype == "Packed Item": + incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate')) + if d.incoming_rate != incoming_rate: + d.incoming_rate = incoming_rate + else: + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 d.discount_amount = 0 frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") From cd36ba7e64343c6997a5aa710196af63fea573fc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:34:00 +0530 Subject: [PATCH 270/376] fix: Error while fetching item taxes --- erpnext/stock/get_item_details.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c64084fe340..e0a0c4a472e 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -438,6 +438,10 @@ def get_barcode_data(items_list): @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} + + if not item_tax_templates: + item_tax_templates = {} + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) From 6eb8d19cc9e3e263a90cde4fb1cf7f0abae21f21 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:38:06 +0530 Subject: [PATCH 271/376] fix: Check for is None --- erpnext/stock/get_item_details.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e0a0c4a472e..ca174a3f63c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -439,8 +439,11 @@ def get_barcode_data(items_list): def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if not item_tax_templates: + if item_tax_templates is None: item_tax_templates = {} + + if item_rates is None: + item_rates = {} if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) @@ -457,7 +460,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) From cae42b48f1deba0905d2687a27a16da87c0ac9d7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:34:00 +0530 Subject: [PATCH 272/376] fix: Error while fetching item taxes --- erpnext/stock/get_item_details.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c64084fe340..e0a0c4a472e 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -438,6 +438,10 @@ def get_barcode_data(items_list): @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} + + if not item_tax_templates: + item_tax_templates = {} + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) From 8dd99c02cc14f81193794fd953b5418ba840b5a2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:38:06 +0530 Subject: [PATCH 273/376] fix: Check for is None --- erpnext/stock/get_item_details.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e0a0c4a472e..ca174a3f63c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -439,8 +439,11 @@ def get_barcode_data(items_list): def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if not item_tax_templates: + if item_tax_templates is None: item_tax_templates = {} + + if item_rates is None: + item_rates = {} if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) @@ -457,7 +460,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) From 923832c31629839ebce067b33fd9d725c87a1ae8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 25 May 2021 17:40:59 +0530 Subject: [PATCH 274/376] chore: remove dead and py2 compatibility code form_grid_template doesn't exist --- erpnext/manufacturing/doctype/work_order/work_order.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2600790a59a..54f4b070980 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1,7 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe import json import math @@ -28,9 +27,6 @@ class ItemHasVariantError(frappe.ValidationError): pass from six import string_types -form_grid_templates = { - "operations": "templates/form_grid/work_order_grid.html" -} class WorkOrder(Document): def onload(self): From 305bbc761912ff96e937c949403178b1af49697c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 14:47:44 +0530 Subject: [PATCH 275/376] fix(ux): show bom in operations child table # Conflicts: # erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json --- .../work_order_operation/work_order_operation.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 8c5cde9a13c..e28a42d39f7 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -2,7 +2,6 @@ "actions": [], "creation": "2014-10-16 14:35:41.950175", "doctype": "DocType", - "editable_grid": 1, "engine": "InnoDB", "field_order": [ "details", @@ -48,6 +47,7 @@ { "fieldname": "bom", "fieldtype": "Link", + "in_list_view": 1, "label": "BOM", "no_copy": 1, "options": "BOM", @@ -67,6 +67,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "Operation completed for how many finished goods?", "fieldname": "completed_qty", "fieldtype": "Float", @@ -76,6 +77,7 @@ "read_only": 1 }, { + "columns": 1, "default": "Pending", "fieldname": "status", "fieldtype": "Select", @@ -118,6 +120,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "in Minutes", "fieldname": "time_in_mins", "fieldtype": "Float", @@ -200,7 +203,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-14 12:58:49.241252", + "modified": "2021-06-24 14:36:12.835543", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -209,4 +212,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 805ac4ffdf554de51c6136279ba55307c0463495 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 12:22:50 +0530 Subject: [PATCH 276/376] fix: order and time of operations for multilevel bom - Order of operations was being sorted by idx of individual operations in BOM table, which made the ordering useless. - This adds ordering that's sorted from lowest level item to top level item. - chore: remove dead functionality. There's no `items` table. Required item level operations get overwritten on fetching of items / operations e.g. when clicking on multi-level BOM checkbox. - test: add test for tree representation - feat: BOMTree class to get complete representation of a tree --- erpnext/manufacturing/doctype/bom/bom.py | 85 ++++++++++++++++++- erpnext/manufacturing/doctype/bom/test_bom.py | 82 +++++++++++++++++- .../doctype/work_order/work_order.py | 57 +++++++------ 3 files changed, 189 insertions(+), 35 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3f109d91b5e..9bb4028374b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1,7 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from typing import List +from collections import deque import frappe, erpnext from frappe.utils import cint, cstr, flt, today from frappe import _ @@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc import functools -from six import string_types - from operator import itemgetter form_grid_templates = { "items": "templates/form_grid/item_grid.html" } + +class BOMTree: + """Full tree representation of a BOM""" + + # specifying the attributes to save resources + # ref: https://docs.python.org/3/reference/datamodel.html#slots + __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + + def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + self.name = name # name of node, BOM number if is_bom else item_code + self.child_items: List["BOMTree"] = [] # list of child items + self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.item_code: str = None # item_code associated with node + self.qty = qty # required unit quantity to make one unit of parent item. + self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. + if not self.is_bom: + self.item_code = self.name + else: + self.__create_tree() + + def __create_tree(self): + bom = frappe.get_cached_doc("BOM", self.name) + self.item_code = bom.item + + for item in bom.get("items", []): + qty = item.qty / bom.quantity # quantity per unit + exploded_qty = self.exploded_qty * qty + if item.bom_no: + child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) + self.child_items.append(child) + else: + self.child_items.append( + BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty) + ) + + def level_order_traversal(self) -> List["BOMTree"]: + """Get level order traversal of tree. + E.g. for following tree the traversal will return list of nodes in order from top to bottom. + BOM: + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 + + returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] + """ + traversal = [] + q = deque() + q.append(self) + + while q: + node = q.popleft() + + for child in node.child_items: + traversal.append(child) + q.append(child) + + return traversal + + def __str__(self) -> str: + return ( + f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}" + f" exploded_qty: {self.exploded_qty}" + ) + + def __repr__(self, level: int = 0) -> str: + rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n" + for child in self.child_items: + rep += child.__repr__(level=level + 1) + return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", @@ -152,7 +224,7 @@ class BOM(WebsiteGenerator): if not args: args = frappe.form_dict.get('args') - if isinstance(args, string_types): + if isinstance(args, str): import json args = json.loads(args) @@ -600,6 +672,11 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + def get_tree_representation(self) -> BOMTree: + """Get a complete tree representation preserving order of child items.""" + return BOMTree(self.name) + + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 1f443fb95ae..57a54587269 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,14 +2,13 @@ # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from collections import deque import unittest import frappe from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost -from six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.tests.test_subcontracting import set_backflush_based_on @@ -227,11 +226,88 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + def test_bom_tree_representation(self): + bom_tree = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + created_tree = parent_bom.get_tree_representation() + + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + created_order = created_tree.level_order_traversal() + + self.assertEqual(len(reqd_order), len(created_order)) + + for reqd_item, created_item in zip(reqd_order, created_order): + self.assertEqual(reqd_item, created_item.item_code) + + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + + + +def level_order_traversal(node): + traversal = [] + q = deque() + q.append(node) + + while q: + node = q.popleft() + + for node_name, subtree in node.items(): + traversal.append(node_name) + q.append(subtree) + + return traversal + +def create_nested_bom(tree, prefix="_Test bom "): + """ Helper function to create a simple nested bom from tree describing item names. (along with required items) + """ + + def create_items(bom_tree): + for item_code, subtree in bom_tree.items(): + bom_item_code = prefix + item_code + if not frappe.db.exists("Item", bom_item_code): + frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() + create_items(subtree) + create_items(tree) + + def dfs(tree, node): + """naive implementation for searching right subtree""" + for node_name, subtree in tree.items(): + if node_name == node: + return subtree + else: + result = dfs(subtree, node) + if result is not None: + return result + + order_of_creating_bom = reversed(level_order_traversal(tree)) + + for item in order_of_creating_bom: + child_items = dfs(tree, item) + if child_items: + bom_item_code = prefix + item + bom = frappe.get_doc(doctype="BOM", item=bom_item_code) + for child_item in child_items.keys(): + bom.append("items", {"item_code": prefix + child_item}) + bom.insert() + bom.submit() + + return bom # parent bom is last bom + + def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): - if warehouse_list and isinstance(warehouse_list, string_types): + if warehouse_list and isinstance(warehouse_list, str): warehouse_list = [warehouse_list] if not warehouse_list: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 54f4b070980..c06cf81ad97 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -389,46 +389,47 @@ class WorkOrder(Document): def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" - self.set('operations', []) + def _get_operations(bom_no, qty=1): + return frappe.db.sql( + f"""select + operation, description, workstation, idx, + base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, + "Pending" as status, parent as bom, batch_size, sequence_id + from + `tabBOM Operation` + where + parent = %s order by idx + """, bom_no, as_dict=1) + + + self.set('operations', []) if not self.bom_no: return - if self.use_multi_level_bom: - bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() + operations = [] + if not self.use_multi_level_bom: + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) else: - bom_list = [self.bom_no] + bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() + bom_traversal = list(reversed(bom_tree.level_order_traversal())) + bom_traversal.append(bom_tree) # add operation on top level item last + + for d in bom_traversal: + if d.is_bom: + operations.extend(_get_operations(d.name, qty=d.exploded_qty)) + + for correct_index, operation in enumerate(operations, start=1): + operation.idx = correct_index - operations = frappe.db.sql(""" - select - operation, description, workstation, idx, - base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id - from - `tabBOM Operation` - where - parent in (%s) order by idx - """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) self.set('operations', operations) - - if self.use_multi_level_bom and self.get('operations') and self.get('items'): - raw_material_operations = [d.operation for d in self.get('items')] - operations = [d.operation for d in self.get('operations')] - - for operation in raw_material_operations: - if operation not in operations: - self.append('operations', { - 'operation': operation - }) - self.calculate_time() def calculate_time(self): - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() From b4e7ee0e45010bac5a783845cb15b46aec4d73c9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 25 May 2021 17:40:59 +0530 Subject: [PATCH 277/376] chore: remove dead and py2 compatibility code form_grid_template doesn't exist --- erpnext/manufacturing/doctype/work_order/work_order.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index e343ed2dd38..302753214b3 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1,7 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe import json import math @@ -30,9 +29,6 @@ class ItemHasVariantError(frappe.ValidationError): pass class SerialNoQtyError(frappe.ValidationError): pass -form_grid_templates = { - "operations": "templates/form_grid/work_order_grid.html" -} class WorkOrder(Document): def onload(self): From 9af3f12411cbdadb0611a10c2bfb4edef0b876ab Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 14:47:44 +0530 Subject: [PATCH 278/376] fix(ux): show bom in operations child table --- .../work_order_operation/work_order_operation.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 6d8fb80e319..f7b8787a0b3 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -2,7 +2,6 @@ "actions": [], "creation": "2014-10-16 14:35:41.950175", "doctype": "DocType", - "editable_grid": 1, "engine": "InnoDB", "field_order": [ "details", @@ -49,6 +48,7 @@ { "fieldname": "bom", "fieldtype": "Link", + "in_list_view": 1, "label": "BOM", "no_copy": 1, "options": "BOM", @@ -68,6 +68,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "Operation completed for how many finished goods?", "fieldname": "completed_qty", "fieldtype": "Float", @@ -77,6 +78,7 @@ "read_only": 1 }, { + "columns": 1, "default": "Pending", "fieldname": "status", "fieldtype": "Select", @@ -119,6 +121,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "in Minutes", "fieldname": "time_in_mins", "fieldtype": "Float", @@ -205,7 +208,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-12 14:48:31.061286", + "modified": "2021-06-24 14:36:12.835543", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -214,4 +217,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 6588a936d5dd96f434ca3590ff8eb01ae3e594fa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 12:22:50 +0530 Subject: [PATCH 279/376] fix: order and time of operations for multilevel bom - Order of operations was being sorted by idx of individual operations in BOM table, which made the ordering useless. - This adds ordering that's sorted from lowest level item to top level item. - chore: remove dead functionality. There's no `items` table. Required item level operations get overwritten on fetching of items / operations e.g. when clicking on multi-level BOM checkbox. - test: add test for tree representation - feat: BOMTree class to get complete representation of a tree --- erpnext/manufacturing/doctype/bom/bom.py | 85 ++++++++++++++++++- erpnext/manufacturing/doctype/bom/test_bom.py | 82 +++++++++++++++++- .../doctype/work_order/work_order.py | 57 +++++++------ 3 files changed, 189 insertions(+), 35 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3e855603b48..c58f017258e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1,7 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from typing import List +from collections import deque import frappe, erpnext from frappe.utils import cint, cstr, flt, today from frappe import _ @@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc import functools -from six import string_types - from operator import itemgetter form_grid_templates = { "items": "templates/form_grid/item_grid.html" } + +class BOMTree: + """Full tree representation of a BOM""" + + # specifying the attributes to save resources + # ref: https://docs.python.org/3/reference/datamodel.html#slots + __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + + def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + self.name = name # name of node, BOM number if is_bom else item_code + self.child_items: List["BOMTree"] = [] # list of child items + self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.item_code: str = None # item_code associated with node + self.qty = qty # required unit quantity to make one unit of parent item. + self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. + if not self.is_bom: + self.item_code = self.name + else: + self.__create_tree() + + def __create_tree(self): + bom = frappe.get_cached_doc("BOM", self.name) + self.item_code = bom.item + + for item in bom.get("items", []): + qty = item.qty / bom.quantity # quantity per unit + exploded_qty = self.exploded_qty * qty + if item.bom_no: + child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) + self.child_items.append(child) + else: + self.child_items.append( + BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty) + ) + + def level_order_traversal(self) -> List["BOMTree"]: + """Get level order traversal of tree. + E.g. for following tree the traversal will return list of nodes in order from top to bottom. + BOM: + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 + + returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] + """ + traversal = [] + q = deque() + q.append(self) + + while q: + node = q.popleft() + + for child in node.child_items: + traversal.append(child) + q.append(child) + + return traversal + + def __str__(self) -> str: + return ( + f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}" + f" exploded_qty: {self.exploded_qty}" + ) + + def __repr__(self, level: int = 0) -> str: + rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n" + for child in self.child_items: + rep += child.__repr__(level=level + 1) + return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", @@ -152,7 +224,7 @@ class BOM(WebsiteGenerator): if not args: args = frappe.form_dict.get('args') - if isinstance(args, string_types): + if isinstance(args, str): import json args = json.loads(args) @@ -600,6 +672,11 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + def get_tree_representation(self) -> BOMTree: + """Get a complete tree representation preserving order of child items.""" + return BOMTree(self.name) + + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 1f443fb95ae..57a54587269 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,14 +2,13 @@ # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from collections import deque import unittest import frappe from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost -from six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.tests.test_subcontracting import set_backflush_based_on @@ -227,11 +226,88 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + def test_bom_tree_representation(self): + bom_tree = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + created_tree = parent_bom.get_tree_representation() + + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + created_order = created_tree.level_order_traversal() + + self.assertEqual(len(reqd_order), len(created_order)) + + for reqd_item, created_item in zip(reqd_order, created_order): + self.assertEqual(reqd_item, created_item.item_code) + + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + + + +def level_order_traversal(node): + traversal = [] + q = deque() + q.append(node) + + while q: + node = q.popleft() + + for node_name, subtree in node.items(): + traversal.append(node_name) + q.append(subtree) + + return traversal + +def create_nested_bom(tree, prefix="_Test bom "): + """ Helper function to create a simple nested bom from tree describing item names. (along with required items) + """ + + def create_items(bom_tree): + for item_code, subtree in bom_tree.items(): + bom_item_code = prefix + item_code + if not frappe.db.exists("Item", bom_item_code): + frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() + create_items(subtree) + create_items(tree) + + def dfs(tree, node): + """naive implementation for searching right subtree""" + for node_name, subtree in tree.items(): + if node_name == node: + return subtree + else: + result = dfs(subtree, node) + if result is not None: + return result + + order_of_creating_bom = reversed(level_order_traversal(tree)) + + for item in order_of_creating_bom: + child_items = dfs(tree, item) + if child_items: + bom_item_code = prefix + item + bom = frappe.get_doc(doctype="BOM", item=bom_item_code) + for child_item in child_items.keys(): + bom.append("items", {"item_code": prefix + child_item}) + bom.insert() + bom.submit() + + return bom # parent bom is last bom + + def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): - if warehouse_list and isinstance(warehouse_list, string_types): + if warehouse_list and isinstance(warehouse_list, str): warehouse_list = [warehouse_list] if not warehouse_list: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 302753214b3..180815d80e4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -468,46 +468,47 @@ class WorkOrder(Document): def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" - self.set('operations', []) + def _get_operations(bom_no, qty=1): + return frappe.db.sql( + f"""select + operation, description, workstation, idx, + base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, + "Pending" as status, parent as bom, batch_size, sequence_id + from + `tabBOM Operation` + where + parent = %s order by idx + """, bom_no, as_dict=1) + + + self.set('operations', []) if not self.bom_no: return - if self.use_multi_level_bom: - bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() + operations = [] + if not self.use_multi_level_bom: + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) else: - bom_list = [self.bom_no] + bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() + bom_traversal = list(reversed(bom_tree.level_order_traversal())) + bom_traversal.append(bom_tree) # add operation on top level item last + + for d in bom_traversal: + if d.is_bom: + operations.extend(_get_operations(d.name, qty=d.exploded_qty)) + + for correct_index, operation in enumerate(operations, start=1): + operation.idx = correct_index - operations = frappe.db.sql(""" - select - operation, description, workstation, idx, - base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id - from - `tabBOM Operation` - where - parent in (%s) order by idx - """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) self.set('operations', operations) - - if self.use_multi_level_bom and self.get('operations') and self.get('items'): - raw_material_operations = [d.operation for d in self.get('items')] - operations = [d.operation for d in self.get('operations')] - - for operation in raw_material_operations: - if operation not in operations: - self.append('operations', { - 'operation': operation - }) - self.calculate_time() def calculate_time(self): - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() From ca2dbcec593ba3d1845d038721790420db114189 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 26 Jun 2021 01:32:17 +0530 Subject: [PATCH 280/376] fix(Asset Repair): Rearrange fields --- erpnext/assets/doctype/asset_repair/asset_repair.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 0651deb2acc..ba3189887ce 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -8,10 +8,10 @@ "engine": "InnoDB", "field_order": [ "asset", - "naming_series", + "company", "column_break_2", "asset_name", - "company", + "naming_series", "section_break_5", "failure_date", "repair_status", @@ -264,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-21 14:53:46.665576", + "modified": "2021-06-25 13:14:38.307723", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 5d5dc56f94a00cf501dc3df0839020216d521cfd Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 22 Jun 2021 15:23:04 +0530 Subject: [PATCH 281/376] fix: removed values out of sync validation from stock transactions --- erpnext/controllers/stock_controller.py | 5 +- .../incorrect_stock_value_report/__init__.py | 0 .../incorrect_stock_value_report.js | 36 +++++ .../incorrect_stock_value_report.json | 29 ++++ .../incorrect_stock_value_report.py | 141 ++++++++++++++++++ 5 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 erpnext/stock/report/incorrect_stock_value_report/__init__.py create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 35097b97b99..8196cff849d 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map -from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map from erpnext.stock.stock_ledger import get_valuation_rate @@ -497,9 +497,6 @@ class StockController(AccountsController): }) if future_sle_exists(args): create_repost_item_valuation_entry(args) - elif not is_reposting_pending(): - check_if_stock_and_account_balance_synced(self.posting_date, - self.company, self.doctype, self.name) @frappe.whitelist() def make_quality_inspections(doctype, docname, items): diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js new file mode 100644 index 00000000000..ff424807e3e --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js @@ -0,0 +1,36 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Stock Value Report"] = { + "filters": [ + { + "label": __("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "label": __("Account"), + "fieldname": "account", + "fieldtype": "Link", + "options": "Account", + get_query: function() { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "account_type": "Stock", + "company": company + } + } + } + }, + { + "label": __("From Date"), + "fieldname": "from_date", + "fieldtype": "Date" + } + ] +}; diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json new file mode 100644 index 00000000000..a7e9f203f7b --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-06-22 15:35:05.148177", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-06-22 15:35:05.148177", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Stock Value Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Stock Value Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py new file mode 100644 index 00000000000..a7243878eb8 --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -0,0 +1,141 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import erpnext +from frappe import _ +from six import iteritems +from frappe.utils import add_days, today, getdate +from erpnext.stock.utils import get_stock_value_on +from erpnext.accounts.utils import get_stock_and_account_balance + +def execute(filters=None): + if not erpnext.is_perpetual_inventory_enabled(filters.company): + frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") + .format(filters.company)) + + data = get_data(filters) + columns = get_columns(filters) + + return columns, data + +def get_unsync_date(filters): + date = filters.from_date + if not date: + date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") + date = date[0][0] + + if not date: + return + + while getdate(date) < getdate(today()): + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date, + company=filters.company, account = filters.account) + + if abs(account_bal - stock_bal) > 0.1: + return date + + date = add_days(date, 1) + +def get_data(report_filters): + from_date = get_unsync_date(report_filters) + + if not from_date: + return [] + + result = [] + + voucher_wise_dict = {} + data = frappe.db.sql(''' + SELECT + name, posting_date, posting_time, voucher_type, voucher_no, + stock_value_difference, stock_value, warehouse, item_code + FROM + `tabStock Ledger Entry` + WHERE + posting_date + = %s and company = %s + and is_cancelled = 0 + ORDER BY timestamp(posting_date, posting_time) asc, creation asc + ''', (from_date, report_filters.company), as_dict=1) + + for d in data: + voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) + + closing_date = add_days(from_date, -1) + for key, stock_data in iteritems(voucher_wise_dict): + prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1]) + for data in stock_data: + expected_stock_value = prev_stock_value + data.stock_value_difference + if abs(data.stock_value - expected_stock_value) > 0.1: + data.difference_value = abs(data.stock_value - expected_stock_value) + data.expected_stock_value = expected_stock_value + result.append(data) + + return result + +def get_columns(filters): + return [ + { + "label": _("Stock Ledger ID"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Stock Ledger Entry", + "width": "80" + }, + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date" + }, + { + "label": _("Posting Time"), + "fieldname": "posting_time", + "fieldtype": "Time" + }, + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "width": "110" + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": "110" + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": "110" + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": "110" + }, + { + "label": _("Expected Stock Value"), + "fieldname": "expected_stock_value", + "fieldtype": "Currency", + "width": "150" + }, + { + "label": _("Stock Value"), + "fieldname": "stock_value", + "fieldtype": "Currency", + "width": "120" + }, + { + "label": _("Difference Value"), + "fieldname": "difference_value", + "fieldtype": "Currency", + "width": "150" + } + ] \ No newline at end of file From 478360397d903bcb374d5cb7fd862337cedc59ea Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Jun 2021 23:13:54 +0530 Subject: [PATCH 282/376] fix: fetch batch items in stock reco --- .../doctype/work_order/test_work_order.py | 9 +- .../stock_reconciliation.js | 67 +++++++---- .../stock_reconciliation.py | 108 +++++++++++++----- 3 files changed, 125 insertions(+), 59 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index cb1ee92196f..68de0b29d3e 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase): ste.submit() stock_entries.append(ste) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') self.assertEqual(len(job_cards), len(bom.operations)) for i, job_card in enumerate(job_cards): doc = frappe.get_doc("Job Card", job_card) - doc.append("time_logs", { - "from_time": add_to_date(None, i), - "hours": 1, - "to_time": add_to_date(None, i + 1), - "completed_qty": doc.for_quantity - }) + doc.time_logs[0].completed_qty = 1 doc.submit() ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index ac4ed5e75d9..a01db80da4a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -48,37 +48,54 @@ frappe.ui.form.on("Stock Reconciliation", { }, get_items: function(frm) { - frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1, + let fields = [{ + label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1, "get_query": function() { return { "filters": { "company": frm.doc.company, } - } - }}, - function(data) { - frappe.call({ - method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", - args: { - warehouse: data.warehouse, - posting_date: frm.doc.posting_date, - posting_time: frm.doc.posting_time, - company:frm.doc.company - }, - callback: function(r) { - var items = []; - frm.clear_table("items"); - for(var i=0; i< r.message.length; i++) { - var d = frm.add_child("items"); - $.extend(d, r.message[i]); - if(!d.qty) d.qty = null; - if(!d.valuation_rate) d.valuation_rate = null; - } - frm.refresh_field("items"); - } - }); + }; } - , __("Get Items"), __("Update")); + }, { + label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item", + "get_query": function() { + return { + "filters": { + "disabled": 0, + } + }; + } + }]; + + frappe.prompt(fields, function(data) { + frappe.call({ + method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", + args: { + warehouse: data.warehouse, + posting_date: frm.doc.posting_date, + posting_time: frm.doc.posting_time, + company: frm.doc.company, + item_code: data.item_code + }, + callback: function(r) { + frm.clear_table("items"); + for (var i=0; i= %s and rgt <= %s and name=bin.warehouse) - """, (lft, rgt)) + where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1 + and i.has_variants = 0 and exists( + select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse + ) + """, (lft, rgt), as_dict=1) items += frappe.db.sql(""" - select i.name, i.item_name, id.default_warehouse, i.has_serial_no + select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no from tabItem i, `tabItem Default` id where i.name = id.parent and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) - and i.is_stock_item = 1 and i.has_batch_no = 0 - and i.has_variants = 0 and i.disabled = 0 and id.company=%s + and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s group by i.name - """, (lft, rgt, company)) + """, (lft, rgt, company), as_dict=1) - res = [] - for d in set(items): - stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time, - with_valuation_rate=True , with_serial_no=cint(d[3])) + return items - if frappe.db.get_value("Item", d[0], "disabled") == 0: - res.append({ - "item_code": d[0], - "warehouse": d[2], - "qty": stock_bal[0], - "item_name": d[1], - "valuation_rate": stock_bal[1], - "current_qty": stock_bal[0], - "current_valuation_rate": stock_bal[1], - "current_serial_no": stock_bal[2] if cint(d[3]) else '', - "serial_no": stock_bal[2] if cint(d[3]) else '' - }) +def get_item_data(row, qty, valuation_rate, serial_no=None): + return { + 'item_code': row.item_code, + 'warehouse': row.warehouse, + 'qty': qty, + 'item_name': row.item_name, + 'valuation_rate': valuation_rate, + 'current_qty': qty, + 'current_valuation_rate': valuation_rate, + 'current_serial_no': serial_no, + 'serial_no': serial_no, + 'batch_no': row.get('batch_no') + } - return res +def get_itemwise_batch(warehouse, posting_date, company, item_code=None): + from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute + itemwise_batch_data = {} + + filters = frappe._dict({ + 'warehouse': warehouse, + 'from_date': posting_date, + 'to_date': posting_date, + 'company': company + }) + + if item_code: + filters.item_code = item_code + + columns, data = execute(filters) + + for row in data: + itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({ + 'item_code': row[0], + 'warehouse': warehouse, + 'qty': row[8], + 'item_name': row[1], + 'batch_no': row[4] + })) + + return itemwise_batch_data @frappe.whitelist() def get_stock_balance_for(item_code, warehouse, From 40c90d03a4e583ff374e180b3abbf95b31f74fed Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 28 Jun 2021 11:42:28 +0530 Subject: [PATCH 283/376] fix(Asset Repair): cancellation --- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 64c51fd8c3e..d32fdf7054f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -30,7 +30,7 @@ class AssetRepair(AccountsController): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) def calculate_total_repair_cost(self): - self.total_repair_cost = self.repair_cost + self.total_repair_cost = flt(self.repair_cost) total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() self.total_repair_cost += total_value_of_stock_consumed @@ -129,6 +129,7 @@ class AssetRepair(AccountsController): def increase_stock_quantity(self): stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + stock_entry.flags.ignore_links = True stock_entry.cancel() def make_gl_entries(self, cancel=False): @@ -252,4 +253,4 @@ class AssetRepair(AccountsController): @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) - return round(downtime, 2) \ No newline at end of file + return round(downtime, 2) From 4277e4c94be69bbb17c4f31bb36df63159f8e286 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 28 Jun 2021 19:10:46 +0530 Subject: [PATCH 284/376] fix: unreachable test case (#26234) --- .../sales_invoice/test_sales_invoice.py | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 114b7d2d352..fe531d3b227 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase): einvoice = make_einvoice(si) validate_totals(einvoice) + def test_item_tax_net_range(self): + item = create_item("T Shirt") + + item.set('taxes', []) + item.append("taxes", { + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + "minimum_net_rate": 0, + "maximum_net_rate": 500 + }) + + item.append("taxes", { + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + "minimum_net_rate": 501, + "maximum_net_rate": 1000 + }) + + item.save() + + sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True) + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") + + # Apply discount + sales_invoice.apply_discount_on = 'Net Total' + sales_invoice.discount_amount = 300 + sales_invoice.save() + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' @@ -1985,33 +2012,6 @@ def get_sales_invoice_for_e_invoice(): return si - def test_item_tax_net_range(self): - item = create_item("T Shirt") - - item.set('taxes', []) - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", - "minimum_net_rate": 0, - "maximum_net_rate": 500 - }) - - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", - "minimum_net_rate": 501, - "maximum_net_rate": 1000 - }) - - item.save() - - sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True) - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") - - # Apply discount - sales_invoice.apply_discount_on = 'Net Total' - sales_invoice.discount_amount = 300 - sales_invoice.save() - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") - def make_test_address_for_ewaybill(): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): address = frappe.get_doc({ From 785a71dcd2444cc085f286140c7595e95ad47d09 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sun, 6 Jun 2021 22:01:18 +0530 Subject: [PATCH 285/376] fix(Purchase Invoice): Resolve difference caused by change in exchange rate --- .../purchase_invoice/purchase_invoice.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 45d89ad1c87..85b46426049 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -634,6 +634,29 @@ class PurchaseInvoice(BuyingController): "project": item.project or self.project }, account_currency, item=item)) + # check if the exchange rate has changed + purchase_receipt_conversion_rate = frappe.db.get_value('Purchase Receipt', {'name': item.purchase_receipt}, ['conversion_rate']) + if self.conversion_rate != purchase_receipt_conversion_rate: + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.rate) * (purchase_receipt_conversion_rate - self.conversion_rate) + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "against": self.supplier, + "debit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + gl_entries.append( + self.get_gl_dict({ + "account": self.get_company_default("exchange_gain_loss_account"), + "against": self.supplier, + "credit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") From 6233eaa3bb6d31df7c47dc0d0be15d52633ddf4a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 05:28:20 +0530 Subject: [PATCH 286/376] fix(Purchase Invoice): Fix condition --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 85b46426049..01df8060acb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -636,7 +636,7 @@ class PurchaseInvoice(BuyingController): # check if the exchange rate has changed purchase_receipt_conversion_rate = frappe.db.get_value('Purchase Receipt', {'name': item.purchase_receipt}, ['conversion_rate']) - if self.conversion_rate != purchase_receipt_conversion_rate: + if purchase_receipt_conversion_rate and self.conversion_rate != purchase_receipt_conversion_rate: discrepancy_caused_by_exchange_rate_difference = (item.qty * item.rate) * (purchase_receipt_conversion_rate - self.conversion_rate) gl_entries.append( self.get_gl_dict({ From 206313d69bfc591afd59ee56b656717543bfd1c1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 06:11:57 +0530 Subject: [PATCH 287/376] fix(Purchase Invoice): Add test for exchange rate difference handling --- .../purchase_invoice/test_purchase_invoice.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2f5d36c8fab..7f350e7ed54 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -230,6 +230,23 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) + def test_purchase_invoice_with_exchange_rate_difference(self): + set_gst_settings() + pr = make_purchase_receipt(currency = "USD", conversion_rate = 70) + pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True") + + for item in pi.items: + item.purchase_receipt = pr.name + + pi.insert() + pi.submit() + + # fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account + gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'}) + voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') + + self.assertEqual(pi.name, voucher_no) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() @@ -1050,6 +1067,24 @@ def update_tax_witholding_category(company, account, date): 'account': account }) tds_category.save() +def set_gst_settings(): + gst_settings = frappe.get_doc("GST Settings") + + gst_account = frappe.get_all( + "GST Account", + fields=["cgst_account", "sgst_account", "igst_account"], + filters = {"company": "_Test Company"} + ) + + if not gst_account: + gst_settings.append("gst_accounts", { + "company": "_Test Company", + "cgst_account": "CGST - _TC", + "sgst_account": "SGST - _TC", + "igst_account": "IGST - _TC", + }) + + gst_settings.save() def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From d97505b277535760b2cf3f2352ea10660dcefd46 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 07:09:11 +0530 Subject: [PATCH 288/376] fix(Purchase Receipt): Resolve difference caused by change in exchange rate --- .../purchase_receipt/purchase_receipt.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e488b695b5f..54cd33242be 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -288,6 +288,10 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, stock_rbnb, account_currency=warehouse_account_currency, item=d) + print("*"* 30) + print(1) + print("warehouse_account_name: ", warehouse_account_name) + print("") # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation @@ -303,6 +307,19 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, account, d.cost_center, -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name, debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d) + + # check if the exchange rate has changed + purchase_invoice_conversion_rate = frappe.db.get_value('Purchase Invoice', {'name': d.purchase_invoice}, ['conversion_rate']) + if purchase_invoice_conversion_rate and self.conversion_rate != purchase_invoice_conversion_rate: + discrepancy_caused_by_exchange_rate_difference = (d.qty * d.rate) * (purchase_invoice_conversion_rate - self.conversion_rate) + + self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) + + self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: From 5314445d58f36e7abb527911e3446ebf909edcc6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 08:02:11 +0530 Subject: [PATCH 289/376] fix(Purchase Receipt): Add test for exchange rate difference handling --- .../purchase_receipt/test_purchase_receipt.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2586a0fc0cb..0ce4c3a6694 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1051,6 +1051,28 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(len(item_two_gl_entry), 1) frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def test_purchase_receipt_with_exchange_rate_difference(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice + + pi = create_purchase_invoice(currency = "USD", conversion_rate = 70) + + create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", + properties={"account": '_Test Account Stock In Hand - TCP1'}) + pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', + company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80, + do_not_save = "True") + + for item in pr.items: + item.purchase_invoice = pi.name + + pr.insert() + pr.submit() + + # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account + gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'}) + voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') + + self.assertEqual(pr.name, voucher_no) def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference From feed97ec53767e777b8a638d5a8e3ffc0a04e500 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Jun 2021 17:27:43 +0530 Subject: [PATCH 290/376] fix: Test --- ...tracted_raw_materials_to_be_transferred.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 2448e17c50f..f1784925c57 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -60,6 +60,7 @@ def transfer_subcontracted_raw_materials(po): rm_item = [ { 'name': po.supplied_items[0].name, +<<<<<<< HEAD 'item_code': item_1, 'rm_item_code': item_1, 'item_name': item_1, @@ -67,10 +68,20 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_1], +======= + 'item_code': '_Test Item Home Desktop 100', + 'rm_item_code': '_Test Item Home Desktop 100', + 'item_name': '_Test Item Home Desktop 100', + 'qty': 2, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 200, +>>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' }, { 'name': po.supplied_items[1].name, +<<<<<<< HEAD 'item_code': item_2, 'rm_item_code': item_2, 'item_name': item_2, @@ -78,6 +89,15 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_2], +======= + 'item_code': '_Test Item', + 'rm_item_code': '_Test Item', + 'item_name': '_Test Item', + 'qty': 1, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 100, +>>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' } ] From 3f5e7bf6d703807f6774d86839f39f6587b1f576 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 06:13:38 +0530 Subject: [PATCH 291/376] fix(Purchase Invoice): Fetch Purchase Receipt details using a function --- .../purchase_invoice/purchase_invoice.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 01df8060acb..32e2eb82f85 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,6 +517,8 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] + purchase_receipt_details = self.get_purchase_receipt_details() + for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) @@ -634,10 +636,13 @@ class PurchaseInvoice(BuyingController): "project": item.project or self.project }, account_currency, item=item)) - # check if the exchange rate has changed - purchase_receipt_conversion_rate = frappe.db.get_value('Purchase Receipt', {'name': item.purchase_receipt}, ['conversion_rate']) - if purchase_receipt_conversion_rate and self.conversion_rate != purchase_receipt_conversion_rate: - discrepancy_caused_by_exchange_rate_difference = (item.qty * item.rate) * (purchase_receipt_conversion_rate - self.conversion_rate) + if purchase_receipt_details[item.item_code]["conversion_rate"] and \ + self.conversion_rate != purchase_receipt_details[item.item_code]["conversion_rate"] and \ + item.net_rate == purchase_receipt_details[item.item_code]["net_rate"]: + + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ + (purchase_receipt_details[item.item_code]["conversion_rate"] - self.conversion_rate) + gl_entries.append( self.get_gl_dict({ "account": expense_account, @@ -710,6 +715,23 @@ class PurchaseInvoice(BuyingController): self.negative_expense_to_be_booked += flt(item.item_tax_amount, \ item.precision("item_tax_amount")) + def get_purchase_receipt_details(self): + purchase_receipt_details = {} + for item in self.items: + if item.purchase_receipt: + purchase_receipt = frappe.get_doc('Purchase Receipt', item.purchase_receipt) + pr_item_details = { + "conversion_rate" : purchase_receipt.conversion_rate + } + + for pr_item in purchase_receipt.items: + if pr_item.item_code == item.item_code: + pr_item_details["net_rate"] = pr_item.net_rate + + purchase_receipt_details[item.item_code] = pr_item_details + + return purchase_receipt_details + def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") From a826fb8f95144a9e14d961bbe6d779c4c5aa668c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 06:18:35 +0530 Subject: [PATCH 292/376] fix(Purchase Receipt): Remove print statements --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 54cd33242be..305eb02714b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -287,11 +287,7 @@ class PurchaseReceipt(BuyingController): continue self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, - stock_rbnb, account_currency=warehouse_account_currency, item=d) - print("*"* 30) - print(1) - print("warehouse_account_name: ", warehouse_account_name) - print("") + stock_rbnb, account_currency=warehouse_account_currency, item=d) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation From 8f52db788a898262c8b546cc42a31e86f1a43560 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 06:38:39 +0530 Subject: [PATCH 293/376] fix(Purchase Invoice): Improve test for exchange rate difference handling --- .../doctype/purchase_invoice/test_purchase_invoice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7f350e7ed54..c38b9017c86 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -247,6 +247,11 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(pi.name, voucher_no) + exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) + + self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() From 506d5ac3d58e6266037f16e9fab4dec506171334 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 28 Jun 2021 15:08:28 +0530 Subject: [PATCH 294/376] fix: Make exchange rate handling more efficient --- .../purchase_invoice/purchase_invoice.py | 77 ++++++++++--------- .../purchase_receipt/purchase_receipt.py | 48 +++++++++--- 2 files changed, 78 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 32e2eb82f85..05d632219ac 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - purchase_receipt_details = self.get_purchase_receipt_details() + exchange_rate_map, net_rate_map = self.get_purchase_receipt_details() for item in self.get("items"): if flt(item.base_net_amount): @@ -636,31 +636,33 @@ class PurchaseInvoice(BuyingController): "project": item.project or self.project }, account_currency, item=item)) - if purchase_receipt_details[item.item_code]["conversion_rate"] and \ - self.conversion_rate != purchase_receipt_details[item.item_code]["conversion_rate"] and \ - item.net_rate == purchase_receipt_details[item.item_code]["net_rate"]: + # check if the exchange rate has changed + if item.get('purchase_receipt'): + if exchange_rate_map[item.purchase_receipt] and \ + self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \ + item.net_rate == net_rate_map[item.item_code]: - discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ - (purchase_receipt_details[item.item_code]["conversion_rate"] - self.conversion_rate) + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ + (exchange_rate_map[item.purchase_receipt] - self.conversion_rate) - gl_entries.append( - self.get_gl_dict({ - "account": expense_account, - "against": self.supplier, - "debit": discrepancy_caused_by_exchange_rate_difference, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) - gl_entries.append( - self.get_gl_dict({ - "account": self.get_company_default("exchange_gain_loss_account"), - "against": self.supplier, - "credit": discrepancy_caused_by_exchange_rate_difference, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "against": self.supplier, + "debit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + gl_entries.append( + self.get_gl_dict({ + "account": self.get_company_default("exchange_gain_loss_account"), + "against": self.supplier, + "credit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: @@ -716,21 +718,22 @@ class PurchaseInvoice(BuyingController): item.precision("item_tax_amount")) def get_purchase_receipt_details(self): - purchase_receipt_details = {} - for item in self.items: - if item.purchase_receipt: - purchase_receipt = frappe.get_doc('Purchase Receipt', item.purchase_receipt) - pr_item_details = { - "conversion_rate" : purchase_receipt.conversion_rate - } - - for pr_item in purchase_receipt.items: - if pr_item.item_code == item.item_code: - pr_item_details["net_rate"] = pr_item.net_rate + purchase_receipts = [] + pr_items = [] - purchase_receipt_details[item.item_code] = pr_item_details + for item in self.get('items'): + if item.get('purchase_receipt'): + purchase_receipts.append(item.purchase_receipt) + if item.get('pr_detail'): + pr_items.append(item.pr_detail) + + exchange_rate_map = frappe._dict(frappe.get_all('Purchase Receipt', filters={'name': ('in', + purchase_receipts)}, fields=['name', 'conversion_rate'], as_list=1)) - return purchase_receipt_details + net_rate_map = frappe._dict(frappe.get_all('Purchase Receipt Item', filters={'name': ('in', + pr_items)}, fields=['item_code', 'net_rate'], as_list=1)) + + return exchange_rate_map, net_rate_map def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 305eb02714b..7b002b0fc61 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -262,6 +262,8 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() + exchange_rate_map, net_rate_map = self.get_purchase_invoice_details() + for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if warehouse_account.get(d.warehouse): @@ -303,19 +305,23 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, account, d.cost_center, -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name, debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d) - + # check if the exchange rate has changed - purchase_invoice_conversion_rate = frappe.db.get_value('Purchase Invoice', {'name': d.purchase_invoice}, ['conversion_rate']) - if purchase_invoice_conversion_rate and self.conversion_rate != purchase_invoice_conversion_rate: - discrepancy_caused_by_exchange_rate_difference = (d.qty * d.rate) * (purchase_invoice_conversion_rate - self.conversion_rate) + if d.get('purchase_invoice'): + if exchange_rate_map[d.purchase_invoice] and \ + self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \ + d.net_rate == net_rate_map[d.item_code]: - self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, item=d) + discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \ + (exchange_rate_map[d.purchase_invoice] - self.conversion_rate) - self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, item=d) + self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) + + self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: @@ -495,6 +501,28 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), remarks, expenses_included_in_asset_valuation, project=item.project, item=item) + def get_purchase_invoice_details(self): + purchase_invoices = [] + pi_items = [] + + for item in self.get('items'): + if item.get('purchase_invoice'): + purchase_invoices.append(item.purchase_invoice) + if item.get('purchase_invoice_item'): + pi_items.append(item.purchase_invoice_item) + + exchange_rate_map = frappe._dict(frappe.get_all('Purchase Invoice', filters={'name': ('in', + purchase_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) + print("*"*50) + print("In get_purchase_invoice_details:") + print("exchange_rate_map: ", exchange_rate_map) + + net_rate_map = frappe._dict(frappe.get_all('Purchase Invoice Item', filters={'name': ('in', + pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) + print("net_rate_map: ", net_rate_map) + + return exchange_rate_map, net_rate_map + def update_assets(self, item, valuation_rate): assets = frappe.db.get_all('Asset', filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } From d748e7f49cbbdf46e6eabf4c6168ecb80cbe065a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 28 Jun 2021 15:33:15 +0530 Subject: [PATCH 295/376] fix: Create common function for fetching conversion rate details of linked docs --- .../purchase_invoice/purchase_invoice.py | 50 ++++++++++++------- .../purchase_receipt/purchase_receipt.py | 26 ++-------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 05d632219ac..c1648686785 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - exchange_rate_map, net_rate_map = self.get_purchase_receipt_details() + exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) for item in self.get("items"): if flt(item.base_net_amount): @@ -717,24 +717,6 @@ class PurchaseInvoice(BuyingController): self.negative_expense_to_be_booked += flt(item.item_tax_amount, \ item.precision("item_tax_amount")) - def get_purchase_receipt_details(self): - purchase_receipts = [] - pr_items = [] - - for item in self.get('items'): - if item.get('purchase_receipt'): - purchase_receipts.append(item.purchase_receipt) - if item.get('pr_detail'): - pr_items.append(item.pr_detail) - - exchange_rate_map = frappe._dict(frappe.get_all('Purchase Receipt', filters={'name': ('in', - purchase_receipts)}, fields=['name', 'conversion_rate'], as_list=1)) - - net_rate_map = frappe._dict(frappe.get_all('Purchase Receipt Item', filters={'name': ('in', - pr_items)}, fields=['item_code', 'net_rate'], as_list=1)) - - return exchange_rate_map, net_rate_map - def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") @@ -1189,6 +1171,36 @@ class PurchaseInvoice(BuyingController): if update: self.db_set('status', self.status, update_modified = update_modified) +# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling +def get_pr_or_pi_details(doc): + if doc.doctype == 'Purchase Invoice': + pr_or_pi = 'purchase_receipt' + items_reference = 'pr_detail' + pr_or_pi_doctype = 'Purchase Receipt' + pr_or_pi_items_table = 'Purchase Receipt Item' + else: + pr_or_pi = 'purchase_invoice' + items_reference = 'purchase_invoice_item' + pr_or_pi_doctype = 'Purchase Invoice' + pr_or_pi_items_table = 'Purchase Invoice Item' + + purchase_receipts_or_invoices = [] + pr_or_pi_items = [] + + for item in doc.get('items'): + if item.get(pr_or_pi): + purchase_receipts_or_invoices.append(item.get(pr_or_pi)) + if item.get(items_reference): + pr_or_pi_items.append(item.get(items_reference)) + + exchange_rate_map = frappe._dict(frappe.get_all(pr_or_pi_doctype, filters={'name': ('in', + purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) + + net_rate_map = frappe._dict(frappe.get_all(pr_or_pi_items_table, filters={'name': ('in', + pr_or_pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) + + return exchange_rate_map, net_rate_map + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context list_context = get_list_context(context) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7b002b0fc61..12fa5302d59 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -254,6 +254,8 @@ class PurchaseReceipt(BuyingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_pr_or_pi_details + stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") @@ -262,7 +264,7 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() - exchange_rate_map, net_rate_map = self.get_purchase_invoice_details() + exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): @@ -501,28 +503,6 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), remarks, expenses_included_in_asset_valuation, project=item.project, item=item) - def get_purchase_invoice_details(self): - purchase_invoices = [] - pi_items = [] - - for item in self.get('items'): - if item.get('purchase_invoice'): - purchase_invoices.append(item.purchase_invoice) - if item.get('purchase_invoice_item'): - pi_items.append(item.purchase_invoice_item) - - exchange_rate_map = frappe._dict(frappe.get_all('Purchase Invoice', filters={'name': ('in', - purchase_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) - print("*"*50) - print("In get_purchase_invoice_details:") - print("exchange_rate_map: ", exchange_rate_map) - - net_rate_map = frappe._dict(frappe.get_all('Purchase Invoice Item', filters={'name': ('in', - pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) - print("net_rate_map: ", net_rate_map) - - return exchange_rate_map, net_rate_map - def update_assets(self, item, valuation_rate): assets = frappe.db.get_all('Asset', filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } From 8f2b8e47f72777576ec330c46d397bd689363031 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 15:58:30 +0530 Subject: [PATCH 296/376] fix: Employee Inactive status implications (#26243) --- erpnext/hr/doctype/attendance/attendance.js | 2 +- erpnext/hr/doctype/attendance/attendance.py | 5 +++++ erpnext/hr/doctype/attendance/attendance_list.js | 3 +++ erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js index c3c3cb82f94..7964078c7f0 100644 --- a/erpnext/hr/doctype/attendance/attendance.js +++ b/erpnext/hr/doctype/attendance/attendance.js @@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { return{ query: "erpnext.controllers.queries.employee_query" - } + } } diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f3b8a799b3c..3412675d811 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -15,6 +15,7 @@ class Attendance(Document): validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) self.validate_attendance_date() self.validate_duplicate_record() + self.validate_employee_status() self.check_leave_record() def validate_attendance_date(self): @@ -38,6 +39,10 @@ class Attendance(Document): frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( frappe.bold(self.employee), frappe.bold(self.attendance_date))) + def validate_employee_status(self): + if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": + frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) + def check_leave_record(self): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 0c7eafe9c61..9a3bac0eb23 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = { label: __('For Employee'), fieldtype: 'Link', options: 'Employee', + get_query: () => { + return {query: "erpnext.controllers.queries.employee_query"} + }, reqd: 1, onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index e71d81f323a..5c7c0a3b092 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -459,6 +459,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): where t1.name = t2.employee and t2.docstatus = 1 + and t1.status != 'Inactive' %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) From 1f10a99910e26c0aca4ac9ba30e3d5f985b992ef Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 15:58:56 +0530 Subject: [PATCH 297/376] fix: Employee Inactive status implications (#26245) --- erpnext/hr/doctype/attendance/attendance.js | 2 +- erpnext/hr/doctype/attendance/attendance.py | 5 +++++ erpnext/hr/doctype/attendance/attendance_list.js | 3 +++ erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js index c3c3cb82f94..7964078c7f0 100644 --- a/erpnext/hr/doctype/attendance/attendance.js +++ b/erpnext/hr/doctype/attendance/attendance.js @@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { return{ query: "erpnext.controllers.queries.employee_query" - } + } } diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f3b8a799b3c..3412675d811 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -15,6 +15,7 @@ class Attendance(Document): validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) self.validate_attendance_date() self.validate_duplicate_record() + self.validate_employee_status() self.check_leave_record() def validate_attendance_date(self): @@ -38,6 +39,10 @@ class Attendance(Document): frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( frappe.bold(self.employee), frappe.bold(self.attendance_date))) + def validate_employee_status(self): + if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": + frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) + def check_leave_record(self): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 0c7eafe9c61..9a3bac0eb23 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = { label: __('For Employee'), fieldtype: 'Link', options: 'Employee', + get_query: () => { + return {query: "erpnext.controllers.queries.employee_query"} + }, reqd: 1, onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index e71d81f323a..5c7c0a3b092 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -459,6 +459,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): where t1.name = t2.employee and t2.docstatus = 1 + and t1.status != 'Inactive' %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) From b4b6c8e2620f36212da55206aa3ff2c9e9fe779c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 29 Jun 2021 17:58:39 +0530 Subject: [PATCH 298/376] Revert "fix: Test" This reverts commit feed97ec53767e777b8a638d5a8e3ffc0a04e500. --- ...tracted_raw_materials_to_be_transferred.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index f1784925c57..2448e17c50f 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -60,7 +60,6 @@ def transfer_subcontracted_raw_materials(po): rm_item = [ { 'name': po.supplied_items[0].name, -<<<<<<< HEAD 'item_code': item_1, 'rm_item_code': item_1, 'item_name': item_1, @@ -68,20 +67,10 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_1], -======= - 'item_code': '_Test Item Home Desktop 100', - 'rm_item_code': '_Test Item Home Desktop 100', - 'item_name': '_Test Item Home Desktop 100', - 'qty': 2, - 'warehouse': '_Test Warehouse - _TC', - 'rate': 100, - 'amount': 200, ->>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' }, { 'name': po.supplied_items[1].name, -<<<<<<< HEAD 'item_code': item_2, 'rm_item_code': item_2, 'item_name': item_2, @@ -89,15 +78,6 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_2], -======= - 'item_code': '_Test Item', - 'rm_item_code': '_Test Item', - 'item_name': '_Test Item', - 'qty': 1, - 'warehouse': '_Test Warehouse - _TC', - 'rate': 100, - 'amount': 100, ->>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' } ] From cf7af62b053ed2ffbe39054b64aafc49557e87b9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Jun 2021 14:14:32 +0530 Subject: [PATCH 299/376] fix: Code cleanup --- .../purchase_invoice/purchase_invoice.py | 32 +++++++++---------- .../purchase_invoice/test_purchase_invoice.py | 23 ++----------- .../purchase_receipt/purchase_receipt.py | 6 ++-- .../purchase_receipt/test_purchase_receipt.py | 15 ++++++--- 4 files changed, 31 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c1648686785..c1cc092554d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) + exchange_rate_map, net_rate_map = get_purchase_document_details(self) for item in self.get("items"): if flt(item.base_net_amount): @@ -640,7 +640,7 @@ class PurchaseInvoice(BuyingController): if item.get('purchase_receipt'): if exchange_rate_map[item.purchase_receipt] and \ self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \ - item.net_rate == net_rate_map[item.item_code]: + item.net_rate == net_rate_map[item.pr_detail]: discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ (exchange_rate_map[item.purchase_receipt] - self.conversion_rate) @@ -1172,32 +1172,32 @@ class PurchaseInvoice(BuyingController): self.db_set('status', self.status, update_modified = update_modified) # to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling -def get_pr_or_pi_details(doc): +def get_purchase_document_details(doc): if doc.doctype == 'Purchase Invoice': - pr_or_pi = 'purchase_receipt' + doc_reference = 'purchase_receipt' items_reference = 'pr_detail' - pr_or_pi_doctype = 'Purchase Receipt' - pr_or_pi_items_table = 'Purchase Receipt Item' + parent_doctype = 'Purchase Receipt' + child_doctype = 'Purchase Receipt Item' else: - pr_or_pi = 'purchase_invoice' + doc_reference = 'purchase_invoice' items_reference = 'purchase_invoice_item' - pr_or_pi_doctype = 'Purchase Invoice' - pr_or_pi_items_table = 'Purchase Invoice Item' + parent_doctype = 'Purchase Invoice' + child_doctype = 'Purchase Invoice Item' purchase_receipts_or_invoices = [] - pr_or_pi_items = [] + items = [] for item in doc.get('items'): - if item.get(pr_or_pi): - purchase_receipts_or_invoices.append(item.get(pr_or_pi)) + if item.get(doc_reference): + purchase_receipts_or_invoices.append(item.get(doc_reference)) if item.get(items_reference): - pr_or_pi_items.append(item.get(items_reference)) + items.append(item.get(items_reference)) - exchange_rate_map = frappe._dict(frappe.get_all(pr_or_pi_doctype, filters={'name': ('in', + exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in', purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) - net_rate_map = frappe._dict(frappe.get_all(pr_or_pi_items_table, filters={'name': ('in', - pr_or_pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) + net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in', + items)}, fields=['name', 'net_rate'], as_list=1)) return exchange_rate_map, net_rate_map diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index c38b9017c86..ec93314c0f6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -231,12 +231,11 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_purchase_invoice_with_exchange_rate_difference(self): - set_gst_settings() pr = make_purchase_receipt(currency = "USD", conversion_rate = 70) pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True") - for item in pi.items: - item.purchase_receipt = pr.name + pi.items[0].purchase_receipt = pr.name + pi.items[0].pr_detail = pr.items[0].name pi.insert() pi.submit() @@ -1072,24 +1071,6 @@ def update_tax_witholding_category(company, account, date): 'account': account }) tds_category.save() -def set_gst_settings(): - gst_settings = frappe.get_doc("GST Settings") - - gst_account = frappe.get_all( - "GST Account", - fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"} - ) - - if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company", - "cgst_account": "CGST - _TC", - "sgst_account": "SGST - _TC", - "igst_account": "IGST - _TC", - }) - - gst_settings.save() def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 12fa5302d59..5ba9c7057bb 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -254,7 +254,7 @@ class PurchaseReceipt(BuyingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): - from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_pr_or_pi_details + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_purchase_document_details stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) @@ -264,7 +264,7 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() - exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) + exchange_rate_map, net_rate_map = get_purchase_document_details(self) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): @@ -312,7 +312,7 @@ class PurchaseReceipt(BuyingController): if d.get('purchase_invoice'): if exchange_rate_map[d.purchase_invoice] and \ self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \ - d.net_rate == net_rate_map[d.item_code]: + d.net_rate == net_rate_map[d.purchase_invoice_item]: discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \ (exchange_rate_map[d.purchase_invoice] - self.conversion_rate) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0ce4c3a6694..d56822a3081 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1051,6 +1051,7 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(len(item_two_gl_entry), 1) frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def test_purchase_receipt_with_exchange_rate_difference(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice @@ -1058,22 +1059,26 @@ class TestPurchaseReceipt(unittest.TestCase): create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", properties={"account": '_Test Account Stock In Hand - TCP1'}) + pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80, do_not_save = "True") - for item in pr.items: - item.purchase_invoice = pi.name + pr.items[0].purchase_invoice = pi.name + pr.items[0].purchase_invoice_item = pi.items[0].name pr.insert() - pr.submit() + pr.submit() # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'}) - voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') - + voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') self.assertEqual(pr.name, voucher_no) + exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) + self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 8492bf040dc5e309c74e9c51121ed6eff519367b Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 30 Jun 2021 17:17:43 +0530 Subject: [PATCH 300/376] fix: feating employee in payroll entry --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 5c7c0a3b092..36e728fc992 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -680,6 +680,10 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] include_employees = [] emp_cond = '' + + if not filters.payroll_frequency: + frappe.throw(_('Select Payroll Frequency.')) + if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) emp = filters.get('employees') From cf4e29a604c819f0673876592c2c9219a1830d0b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 30 Jun 2021 20:27:32 +0530 Subject: [PATCH 301/376] chore: Added change log for v13.6.0 --- erpnext/change_log/v13/v13_6_0.md | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 erpnext/change_log/v13/v13_6_0.md diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md new file mode 100644 index 00000000000..d881b279e3f --- /dev/null +++ b/erpnext/change_log/v13/v13_6_0.md @@ -0,0 +1,72 @@ +# Version 13.6.0 Release Notes + +### Features & Enhancements + +- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523)) +- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044)) +- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184)) +- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878)) +- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705)) +- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030)) +- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696)) +- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891)) + +### Fixes + +- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) +- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176)) +- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092)) +- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978)) +- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073)) +- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245)) +- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230)) +- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125)) +- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134)) +- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196)) +- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083)) +- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941)) +- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945)) +- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011)) +- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070)) +- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071)) +- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122)) +- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220)) +- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003)) +- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229)) +- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269)) +- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045)) +- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170)) +- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032)) +- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095)) +- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023)) +- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191)) +- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188)) +- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217)) +- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152)) +- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108)) +- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202)) +- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906)) +- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894)) +- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997)) +- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051)) +- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043)) +- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143)) +- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211)) +- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126)) +- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192)) +- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081)) +- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187)) +- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195)) +- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947)) +- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951)) +- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968)) +- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037)) +- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198)) +- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100)) +- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098)) +- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062)) +- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031)) +- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203)) +- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185)) +- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934)) +- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201)) From 87b4e6ea323bf242e0661a8735c38d5cc5d4bea8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Jun 2021 23:27:24 +0530 Subject: [PATCH 302/376] fix: employee selection not working in payroll entry --- .../doctype/payroll_entry/payroll_entry.js | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index f2892600d12..496c37b2fad 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', { }); frm.set_query('employee', 'employees', () => { - if (!frm.doc.company) { - frappe.msgprint(__("Please set a Company")); - return []; + let error_fields = []; + let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date']; + + let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]); + + mandatory_fields.forEach(field => { + if (!frm.doc[field]) { + error_fields.push(frappe.unscrub(field)); + } + }); + + if (error_fields && error_fields.length) { + message = message + '

    • ' + error_fields.join('
    • ') + "
    "; + frappe.throw({ + message: message, + indicator: 'red', + title: __('Missing Fields') + }); } + return { query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", filters: frm.events.get_employee_filters(frm) @@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', { get_employee_filters: function (frm) { let filters = {}; - filters['company'] = frm.doc.company; - filters['start_date'] = frm.doc.start_date; - filters['end_date'] = frm.doc.end_date; filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet; - filters['payroll_frequency'] = frm.doc.payroll_frequency; - filters['payroll_payable_account'] = frm.doc.payroll_payable_account; - filters['currency'] = frm.doc.currency; - if (frm.doc.department) { - filters['department'] = frm.doc.department; - } - if (frm.doc.branch) { - filters['branch'] = frm.doc.branch; - } - if (frm.doc.designation) { - filters['designation'] = frm.doc.designation; - } + let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account', + 'currency', 'department', 'branch', 'designation']; + + fields.forEach(field => { + if (frm.doc[field]) { + filters[field] = frm.doc[field]; + } + }); + if (frm.doc.employees) { - filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + if (employees && employees.length) { + filters['employees'] = employees; + } } return filters; }, From f99f872946f178d76a823ac667927555fbdedf03 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 1 Jul 2021 11:50:48 +0530 Subject: [PATCH 303/376] fix: update cost not working in the draft bom --- erpnext/manufacturing/doctype/bom/bom.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 27019dbbae2..15a7c316c91 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -325,8 +325,7 @@ frappe.ui.form.on("BOM", { freeze: true, args: { update_parent: true, - from_child_bom:false, - save: frm.doc.docstatus === 1 ? true : false + from_child_bom:false }, callback: function(r) { refresh_field("items"); From eee03fcbabd1974ddbbefa12e1f5c34a128b371e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 1 Jul 2021 12:57:13 +0550 Subject: [PATCH 304/376] bumped to version 13.6.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 39d9a27615e..0c96d325c2e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.5.2' +__version__ = '13.6.0' def get_default_company(user=None): '''Get default company for user''' From d45e307f02af919d6880a53d1e9e4bf406788ca7 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 1 Jul 2021 12:38:09 +0530 Subject: [PATCH 305/376] test: fix expected test failure (#26275) reference: https://github.com/frappe/frappe/pull/13557 --- .../test_patient_history_settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py index c93b788aed7..33119d8185f 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest import json -from frappe.utils import getdate +from frappe.utils import getdate, strip_html from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient class TestPatientHistorySettings(unittest.TestCase): @@ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase): self.assertTrue(medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) - expected_subject = "Date: {0}
    Rating: 3
    Feedback: Test Patient History Settings
    ".format( + expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format( frappe.utils.format_date(getdate())) - self.assertEqual(medical_rec.subject, expected_subject) + self.assertEqual(strip_html(medical_rec.subject), expected_subject) self.assertEqual(medical_rec.patient, patient) self.assertEqual(medical_rec.communication_date, getdate()) @@ -101,4 +101,4 @@ def create_doc(patient): }).insert() doc.submit() - return doc \ No newline at end of file + return doc From 5a4251107c62cb96a4519585eb49c14fa091d038 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 1 Jul 2021 17:17:34 +0530 Subject: [PATCH 306/376] feat: Project Portal Enhancements (#26090) * fix: project portal enhancements * fix: timesheet table and task nesting * fix: semgrep and link issue * fix: sider * fix: project details view title * fix: project progress pills * fix: website route rule for project * fix: multi level nesting * fix: added subject and indentation Co-authored-by: Rucha Mahabal --- erpnext/hooks.py | 1 + .../includes/projects/project_row.html | 80 ++++--- .../includes/projects/project_tasks.html | 33 +-- .../includes/projects/project_timesheets.html | 54 +++-- erpnext/templates/pages/projects.html | 215 ++++++++++++------ erpnext/templates/pages/projects.py | 44 +--- 6 files changed, 250 insertions(+), 177 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 3da606b68b8..ba10b58f858 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -158,6 +158,7 @@ website_route_rules = [ "parents": [{"label": _("Material Request"), "route": "material-requests"}] } }, + {"from_route": "/project", "to_route": "Project"} ] standard_portal_menu_items = [ diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html index 4c8c40db003..a256fbd677b 100644 --- a/erpnext/templates/includes/projects/project_row.html +++ b/erpnext/templates/includes/projects/project_row.html @@ -1,28 +1,54 @@ -{% if doc.status=="Open" %} - +{% if doc.status == "Open" %} +
    +
    +
    + Link + {{ doc.name }} +
    +
    + {{ doc.project_name }} +
    +
    + {% if doc.percent_complete %} + {% set pill_class = "green" if doc.percent_complete | round == 100 else + "orange" %} +
    + + {{ frappe.utils.cint(doc.percent_complete) }} + % + +
    + {% else %} + + {{ doc.status }} + {% endif %} +
    + {% if doc["_assign"] %} + {% set assigned_users = json.loads(doc["_assign"])%} +
    + {% for user in assigned_users %} + {% set user_details = frappe + .db + .get_value("User", user, [ + "full_name", "user_image" + ], as_dict = True) %} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} + {% endfor %} +
    + {% endif %} +
    + {{ frappe.utils.pretty_date(doc.modified) }} +
    +
    +
    {% endif %} diff --git a/erpnext/templates/includes/projects/project_tasks.html b/erpnext/templates/includes/projects/project_tasks.html index 50b9f4b2597..2b07a5f0d07 100644 --- a/erpnext/templates/includes/projects/project_tasks.html +++ b/erpnext/templates/includes/projects/project_tasks.html @@ -1,32 +1,5 @@ {% for task in doc.tasks %} - +
    + {{ task_row(task, 0) }} +
    {% endfor %} diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html index 05a07c12e8b..fa5b2f9f2e4 100644 --- a/erpnext/templates/includes/projects/project_timesheets.html +++ b/erpnext/templates/includes/projects/project_timesheets.html @@ -1,23 +1,33 @@ {% for timesheet in doc.timesheets %} - -{% endfor %} \ No newline at end of file +
    +
    +
    {{ timesheet.name }}
    + Link +
    {{ timesheet.status }}
    +
    {{ frappe.utils.format_date(timesheet.from_time, "medium") }}
    +
    {{ frappe.utils.format_date(timesheet.to_time, "medium") }}
    +
    + {% set user_details = frappe + .db + .get_value("User", timesheet.modified_by, [ + "full_name", "user_image" + ], as_dict = True) + %} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} +
    +
    + {{ frappe.utils.pretty_date(timesheet.modified) }} +
    +
    +
    +{% endfor %} diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index 7e294e076b3..76eaf75cf30 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -1,90 +1,173 @@ {% extends "templates/web.html" %} -{% block title %}{{ doc.project_name }}{% endblock %} +{% block title %} + {{ doc.project_name }} +{% endblock %} + +{% block head_include %} + +{% endblock %} {% block header %} -

    {{ doc.project_name }}

    +

    {{ doc.project_name }}

    {% endblock %} {% block style %} - + {% endblock %} - {% block page_content %} -{% if doc.percent_complete %} -
    -
    -
    -
    -{% endif %} -
    -

    {{ _("Tasks") }}

    - {{ _("New task") }} -
    + {{ progress_bar(doc.percent_complete) }} -

    - -

    +
    +

    Status:

    +

    Progress: + {{ doc.percent_complete }} + % +

    +

    Hours Spent: + {{ doc.actual_time }} +

    +
    -{% if doc.tasks %} -
    -
    - {% include "erpnext/templates/includes/projects/project_tasks.html" %} -
    -

    -

    -{% else %} -

    {{ _("No tasks") }}

    -{% endif %} + {{ progress_bar(doc.percent_complete) }} + {% if doc.tasks %} +
    +
    +
    +
    +

    Tasks

    +

    Status

    +

    End Date

    +

    Assigned To

    + +
    +
    + {% include "erpnext/templates/includes/projects/project_tasks.html" %} +
    +
    + {% else %} +

    {{ _("No Tasks") }}

    + {% endif %} -
    + {% if doc.timesheets %} +
    +
    +
    +
    +

    Timesheets

    +

    Status

    +

    From

    +

    To

    +

    Modified By

    +

    Modified On

    +
    +
    + {% include "erpnext/templates/includes/projects/project_timesheets.html" %} +
    +
    + {% else %} +

    {{ _("No Timesheets") }}

    + {% endif %} -

    {{ _("Timesheets") }}

    + {% if doc.attachments %} +
    -{% if doc.timesheets %} -
    - {% include "erpnext/templates/includes/projects/project_timesheets.html" %} -
    - {% if doc.timesheets|length > 9 %} -

    {{ _("More") }}

    - {% endif %} -{% else %} -

    {{ _("No time sheets") }}

    -{% endif %} - -{% if doc.attachments %} -
    - -

    {{ _("Attachments") }}

    -
    - {% for attachment in doc.attachments %} - - {% endfor %} -
    -{% endif %} +

    {{ _("Attachments") }}

    +
    + {% for attachment in doc.attachments %} + + {% endfor %} +
    + {% endif %}
    {% endblock %} + +{% macro progress_bar(percent_complete) %} +{% if percent_complete %} +
    +
    +
    +{% else %} +
    +{% endif %} +{% endmacro %} + +{% macro task_row(task, indent) %} +
    + +
    {{ task.status }}
    +
    + {% if task.exp_end_date %} + {{ task.exp_end_date }} + {% else %} + -- + {% endif %} +
    +
    + {% if task["_assign"] %} + {% set assigned_users = json.loads(task["_assign"])%} + {% for user in assigned_users %} + {% set user_details = frappe.db.get_value("User", user, + ["full_name", "user_image"], + as_dict = True)%} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} + {% endfor %} + {% endif %} +
    +
    + {{ frappe.utils.pretty_date(task.modified) }} +
    +
    +{% if task.children %} + {% for child in task.children %} + {{ task_row(child, indent + 30) }} + {% endfor %} +{% endif %} +{% endmacro %} diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index d23fed9e7d1..7ff495402a4 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -32,29 +32,17 @@ def get_tasks(project, start=0, search=None, item_status=None): filters = {"project": project} if search: filters["subject"] = ("like", "%{0}%".format(search)) - # if item_status: -# filters["status"] = item_status tasks = frappe.get_all("Task", filters=filters, - fields=["name", "subject", "status", "_seen", "_comments", "modified", "description"], + fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"], limit_start=start, limit_page_length=10) - + task_nest = [] for task in tasks: - task.todo = frappe.get_all('ToDo',filters={'reference_name':task.name, 'reference_type':'Task'}, - fields=["assigned_by", "owner", "modified", "modified_by"]) - - if task.todo: - task.todo=task.todo[0] - task.todo.user_image = frappe.db.get_value('User', task.todo.owner, 'user_image') - - - task.comment_count = len(json.loads(task._comments or "[]")) - - task.css_seen = '' - if task._seen: - if frappe.session.user in json.loads(task._seen): - task.css_seen = 'seen' - - return tasks + if task.is_group: + child_tasks = list(filter(lambda x: x.parent_task == task.name, tasks)) + if len(child_tasks): + task.children = child_tasks + task_nest.append(task) + return list(filter(lambda x: not x.parent_task, tasks)) @frappe.whitelist() def get_task_html(project, start=0, item_status=None): @@ -74,19 +62,11 @@ def get_timesheets(project, start=0, search=None): fields=['project','activity_type','from_time','to_time','parent'], limit_start=start, limit_page_length=10) for timesheet in timesheets: - timesheet.infos = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, - fields=['name','_comments','_seen','status','modified','modified_by'], + info = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, + fields=['name','status','modified','modified_by'], limit_start=start, limit_page_length=10) - - for timesheet.info in timesheet.infos: - timesheet.info.user_image = frappe.db.get_value('User', timesheet.info.modified_by, 'user_image') - - timesheet.info.comment_count = len(json.loads(timesheet.info._comments or "[]")) - - timesheet.info.css_seen = '' - if timesheet.info._seen: - if frappe.session.user in json.loads(timesheet.info._seen): - timesheet.info.css_seen = 'seen' + if len(info): + timesheet.update(info[0]) return timesheets @frappe.whitelist() From d2c86bb9d7ba07fc306e2b9b5d14c6b769322b6b Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Thu, 1 Jul 2021 19:59:08 +0530 Subject: [PATCH 308/376] fix: Fixed Budget Variance Graph color from all black to default --- .../report/budget_variance_report/budget_variance_report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 9c9ada871c8..f1b231b6901 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data): {'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} ] - } + }, + 'type' : 'bar' } From 74b8c99bc29eb2ffe52ee06bff829cc06ad673c8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 15 Apr 2021 11:30:55 +0530 Subject: [PATCH 309/376] feat: Introduced cypress tests in erpnext --- .eslintrc | 3 + cypress.json | 11 + cypress/fixtures/example.json | 5 + cypress/integration/test_customer.js | 13 ++ cypress/plugins/index.js | 17 ++ cypress/support/commands.js | 326 +++++++++++++++++++++++++++ cypress/support/index.js | 25 ++ cypress/tsconfig.json | 12 + 8 files changed, 412 insertions(+) create mode 100644 cypress.json create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/integration/test_customer.js create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/index.js create mode 100644 cypress/tsconfig.json diff --git a/.eslintrc b/.eslintrc index e40502acd6c..a5fcc1bcba2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -147,11 +147,14 @@ "Chart": true, "Cypress": true, "cy": true, + "describe": true, + "expect": true, "it": true, "context": true, "before": true, "beforeEach": true, "onScan": true, "extend_cscript": true + "localforage": true, } } diff --git a/cypress.json b/cypress.json new file mode 100644 index 00000000000..f7bd9d9a17c --- /dev/null +++ b/cypress.json @@ -0,0 +1,11 @@ +{ + "baseUrl": "http://test-develop:8001/", + "projectId": "92odwv", + "adminPassword": "admin", + "defaultCommandTimeout": 20000, + "pageLoadTimeout": 15000, + "retries": { + "runMode": 2, + "openMode": 2 + } +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 00000000000..da18d9352a1 --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js new file mode 100644 index 00000000000..3d6ed5d0d89 --- /dev/null +++ b/cypress/integration/test_customer.js @@ -0,0 +1,13 @@ + +context('Customer', () => { + before(() => { + cy.login(); + }); + it('Check Customer Group', () => { + cy.visit(`app/customer/`); + cy.get('.primary-action').click(); + cy.wait(500); + cy.get('.custom-actions > .btn').click(); + cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups'); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 00000000000..07d9804a733 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = () => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 00000000000..1964b96d702 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,326 @@ +import 'cypress-file-upload'; +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }); +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); +Cypress.Commands.add('login', (email, password) => { + if (!email) { + email = 'Administrator'; + } + if (!password) { + password = Cypress.config('adminPassword'); + } + cy.request({ + url: '/api/method/login', + method: 'POST', + body: { + usr: email, + pwd: password + } + }); +}); + +Cypress.Commands.add('call', (method, args) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + url: `/api/method/${method}`, + method: 'POST', + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('get_list', (doctype, fields = [], filters = []) => { + filters = JSON.stringify(filters); + fields = JSON.stringify(fields); + let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`; + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'GET', + url, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('get_doc', (doctype, name) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'GET', + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body; + }); + }); +}); + +Cypress.Commands.add('remove_doc', (doctype, name) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'DELETE', + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(202); + return res.body; + }); + }); +}); + +Cypress.Commands.add('create_records', doc => { + return cy + .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc}) + .then(r => r.message); +}); + +Cypress.Commands.add('set_value', (doctype, name, obj) => { + return cy.call('frappe.client.set_value', { + doctype, + name, + fieldname: obj + }); +}); + +Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { + cy.get_field(fieldname, fieldtype).as('input'); + + if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { + cy.get('@input').click().wait(200); + cy.get('.datepickers-container .datepicker.active').should('exist'); + } + if (fieldtype === 'Time') { + cy.get('@input').clear().wait(200); + } + + if (fieldtype === 'Select') { + cy.get('@input').select(value); + } else { + cy.get('@input').type(value, {waitForAnimations: false, force: true}); + } + return cy.get('@input'); +}); + +Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { + let selector = `.form-control[data-fieldname="${fieldname}"]`; + + if (fieldtype === 'Text Editor') { + selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } + if (fieldtype === 'Code') { + selector = `[data-fieldname="${fieldname}"] .ace_text-input`; + } + + return cy.get(selector); +}); + +Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => { + cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input'); + + if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { + cy.get('@input').click().wait(200); + cy.get('.datepickers-container .datepicker.active').should('exist'); + } + if (fieldtype === 'Time') { + cy.get('@input').clear().wait(200); + } + + if (fieldtype === 'Select') { + cy.get('@input').select(value); + } else { + cy.get('@input').type(value, {waitForAnimations: false, force: true}); + } + return cy.get('@input'); +}); + +Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => { + let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`; + selector += ` [data-idx="${row_idx}"]`; + selector += ` .form-in-grid`; + + if (fieldtype === 'Text Editor') { + selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } else if (fieldtype === 'Code') { + selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; + } else { + selector += ` .form-control[data-fieldname="${fieldname}"]`; + } + + return cy.get(selector); +}); + +Cypress.Commands.add('awesomebar', text => { + cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100}); +}); + +Cypress.Commands.add('new_form', doctype => { + let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); + cy.visit(`/app/${dt_in_route}/new`); + cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`); + cy.get('body').should('have.attr', 'data-ajax-state', 'complete'); +}); + +Cypress.Commands.add('go_to_list', doctype => { + cy.visit(`/app/list/${doctype}/list`); +}); + +Cypress.Commands.add('clear_cache', () => { + cy.window() + .its('frappe') + .then(frappe => { + frappe.ui.toolbar.clear_cache(); + }); +}); + +Cypress.Commands.add('dialog', opts => { + return cy.window().then(win => { + var d = new win.frappe.ui.Dialog(opts); + d.show(); + return d; + }); +}); + +Cypress.Commands.add('get_open_dialog', () => { + return cy.get('.modal:visible').last(); +}); + +Cypress.Commands.add('hide_dialog', () => { + cy.wait(300); + cy.get_open_dialog().find('.btn-modal-close').click(); + cy.get('.modal:visible').should('not.exist'); +}); + +Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body.data; + }); + }); +}); + +Cypress.Commands.add('add_filter', () => { + cy.get('.filter-section .filter-button').click(); + cy.wait(300); + cy.get('.filter-popover').should('exist'); +}); + +Cypress.Commands.add('clear_filters', () => { + cy.get('.filter-section .filter-button').click(); + cy.wait(300); + cy.get('.filter-popover').should('exist'); + cy.get('.filter-popover').find('.clear-filters').click(); + cy.get('.filter-section .filter-button').click(); + cy.window().its('cur_list').then(cur_list => { + cur_list && cur_list.filter_area && cur_list.filter_area.clear(); + }); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 00000000000..1bee72d2cae --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,25 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Cookies.defaults({ + preserve: 'sid' +}); \ No newline at end of file diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 00000000000..d90ebf6856d --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": "../node_modules", + "types": [ + "cypress" + ] + }, + "include": [ + "**/*.*" + ] +} \ No newline at end of file From 3f14b92e2cd60a32e51c86a79e7497315cf2530b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 4 Jun 2021 16:41:32 +0530 Subject: [PATCH 310/376] ci: UI tests workflow --- .eslintrc | 2 +- .github/workflows/ui-tests.yml | 101 +++++++++++++++++++++++++++++++++ cypress.json | 2 +- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ui-tests.yml diff --git a/.eslintrc b/.eslintrc index a5fcc1bcba2..cb45ce5f692 100644 --- a/.eslintrc +++ b/.eslintrc @@ -154,7 +154,7 @@ "before": true, "beforeEach": true, "onScan": true, - "extend_cscript": true + "extend_cscript": true, "localforage": true, } } diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 00000000000..b187dff5c5d --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,101 @@ +name: UI + +on: + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + + name: UI Tests (Cypress) + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true + + - name: Add to Hosts + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache cypress binary + uses: actions/cache@v2 + with: + path: ~/.cache + key: ${{ runner.os }}-cypress- + restore-keys: | + ${{ runner.os }}-cypress- + ${{ runner.os }}- + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + DB: mariadb + TYPE: ui + + - name: Site Setup + run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests + + + - name: Build Assets + run: cd ~/frappe-bench/ && bench build + + - name: UI Tests + run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} diff --git a/cypress.json b/cypress.json index f7bd9d9a17c..839bb08fa9e 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,5 @@ { - "baseUrl": "http://test-develop:8001/", + "baseUrl": "http://test_site:8000/", "projectId": "92odwv", "adminPassword": "admin", "defaultCommandTimeout": 20000, From 4d9c08d92ac0bfae1634f8a5058dda3d47250fc9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 29 Jun 2021 21:05:43 +0530 Subject: [PATCH 311/376] chore: add project id for cypress --- cypress.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.json b/cypress.json index 839bb08fa9e..02b10d893f8 100644 --- a/cypress.json +++ b/cypress.json @@ -1,6 +1,6 @@ { "baseUrl": "http://test_site:8000/", - "projectId": "92odwv", + "projectId": "da59y9", "adminPassword": "admin", "defaultCommandTimeout": 20000, "pageLoadTimeout": 15000, From a68344fe8a1e068eea910c70d4f13edf84e1f715 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 29 Jun 2021 21:33:55 +0530 Subject: [PATCH 312/376] refactor: extend commands from frappe --- .github/workflows/ui-tests.yml | 3 + cypress/support/commands.js | 301 --------------------------------- cypress/support/index.js | 3 +- 3 files changed, 5 insertions(+), 302 deletions(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index b187dff5c5d..4bc55da1d8f 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -91,6 +91,9 @@ jobs: - name: Site Setup run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests + - name: cypress pre-requisites + run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile + - name: Build Assets run: cd ~/frappe-bench/ && bench build diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 1964b96d702..7929a2e0ef5 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,4 +1,3 @@ -import 'cypress-file-upload'; // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite @@ -24,303 +23,3 @@ import 'cypress-file-upload'; // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); -Cypress.Commands.add('login', (email, password) => { - if (!email) { - email = 'Administrator'; - } - if (!password) { - password = Cypress.config('adminPassword'); - } - cy.request({ - url: '/api/method/login', - method: 'POST', - body: { - usr: email, - pwd: password - } - }); -}); - -Cypress.Commands.add('call', (method, args) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - url: `/api/method/${method}`, - method: 'POST', - body: args, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(200); - return res.body; - }); - }); -}); - -Cypress.Commands.add('get_list', (doctype, fields = [], filters = []) => { - filters = JSON.stringify(filters); - fields = JSON.stringify(fields); - let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`; - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'GET', - url, - headers: { - Accept: 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(200); - return res.body; - }); - }); -}); - -Cypress.Commands.add('get_doc', (doctype, name) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'GET', - url: `/api/resource/${doctype}/${name}`, - headers: { - Accept: 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(200); - return res.body; - }); - }); -}); - -Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'POST', - url: `/api/resource/${doctype}`, - body: args, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - failOnStatusCode: !ignore_duplicate - }) - .then(res => { - let status_codes = [200]; - if (ignore_duplicate) { - status_codes.push(409); - } - expect(res.status).to.be.oneOf(status_codes); - return res.body; - }); - }); -}); - -Cypress.Commands.add('remove_doc', (doctype, name) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'DELETE', - url: `/api/resource/${doctype}/${name}`, - headers: { - Accept: 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(202); - return res.body; - }); - }); -}); - -Cypress.Commands.add('create_records', doc => { - return cy - .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc}) - .then(r => r.message); -}); - -Cypress.Commands.add('set_value', (doctype, name, obj) => { - return cy.call('frappe.client.set_value', { - doctype, - name, - fieldname: obj - }); -}); - -Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { - cy.get_field(fieldname, fieldtype).as('input'); - - if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { - cy.get('@input').click().wait(200); - cy.get('.datepickers-container .datepicker.active').should('exist'); - } - if (fieldtype === 'Time') { - cy.get('@input').clear().wait(200); - } - - if (fieldtype === 'Select') { - cy.get('@input').select(value); - } else { - cy.get('@input').type(value, {waitForAnimations: false, force: true}); - } - return cy.get('@input'); -}); - -Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { - let selector = `.form-control[data-fieldname="${fieldname}"]`; - - if (fieldtype === 'Text Editor') { - selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; - } - if (fieldtype === 'Code') { - selector = `[data-fieldname="${fieldname}"] .ace_text-input`; - } - - return cy.get(selector); -}); - -Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => { - cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input'); - - if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { - cy.get('@input').click().wait(200); - cy.get('.datepickers-container .datepicker.active').should('exist'); - } - if (fieldtype === 'Time') { - cy.get('@input').clear().wait(200); - } - - if (fieldtype === 'Select') { - cy.get('@input').select(value); - } else { - cy.get('@input').type(value, {waitForAnimations: false, force: true}); - } - return cy.get('@input'); -}); - -Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => { - let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`; - selector += ` [data-idx="${row_idx}"]`; - selector += ` .form-in-grid`; - - if (fieldtype === 'Text Editor') { - selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; - } else if (fieldtype === 'Code') { - selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; - } else { - selector += ` .form-control[data-fieldname="${fieldname}"]`; - } - - return cy.get(selector); -}); - -Cypress.Commands.add('awesomebar', text => { - cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100}); -}); - -Cypress.Commands.add('new_form', doctype => { - let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); - cy.visit(`/app/${dt_in_route}/new`); - cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`); - cy.get('body').should('have.attr', 'data-ajax-state', 'complete'); -}); - -Cypress.Commands.add('go_to_list', doctype => { - cy.visit(`/app/list/${doctype}/list`); -}); - -Cypress.Commands.add('clear_cache', () => { - cy.window() - .its('frappe') - .then(frappe => { - frappe.ui.toolbar.clear_cache(); - }); -}); - -Cypress.Commands.add('dialog', opts => { - return cy.window().then(win => { - var d = new win.frappe.ui.Dialog(opts); - d.show(); - return d; - }); -}); - -Cypress.Commands.add('get_open_dialog', () => { - return cy.get('.modal:visible').last(); -}); - -Cypress.Commands.add('hide_dialog', () => { - cy.wait(300); - cy.get_open_dialog().find('.btn-modal-close').click(); - cy.get('.modal:visible').should('not.exist'); -}); - -Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'POST', - url: `/api/resource/${doctype}`, - body: args, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - failOnStatusCode: !ignore_duplicate - }) - .then(res => { - let status_codes = [200]; - if (ignore_duplicate) { - status_codes.push(409); - } - expect(res.status).to.be.oneOf(status_codes); - return res.body.data; - }); - }); -}); - -Cypress.Commands.add('add_filter', () => { - cy.get('.filter-section .filter-button').click(); - cy.wait(300); - cy.get('.filter-popover').should('exist'); -}); - -Cypress.Commands.add('clear_filters', () => { - cy.get('.filter-section .filter-button').click(); - cy.wait(300); - cy.get('.filter-popover').should('exist'); - cy.get('.filter-popover').find('.clear-filters').click(); - cy.get('.filter-section .filter-button').click(); - cy.window().its('cur_list').then(cur_list => { - cur_list && cur_list.filter_area && cur_list.filter_area.clear(); - }); -}); diff --git a/cypress/support/index.js b/cypress/support/index.js index 1bee72d2cae..72070cc81c4 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -15,6 +15,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import '../../../frappe/cypress/support/commands' // eslint-disable-line // Alternatively you can use CommonJS syntax: @@ -22,4 +23,4 @@ import './commands'; Cypress.Cookies.defaults({ preserve: 'sid' -}); \ No newline at end of file +}); From 8ebf32e18f7f8eb9fda17ef7b0d35d7ea974d0b4 Mon Sep 17 00:00:00 2001 From: Ankush Date: Fri, 2 Jul 2021 11:09:19 +0530 Subject: [PATCH 313/376] fix: undo changes to issue.py (#26291) The fix ported from v13 to develop is not valid because of the new generic SLA feature. Hard resetting the file to the previous version. ec25d5938b2170e557a08991f891e945925943f3 --- erpnext/support/doctype/issue/issue.py | 105 +------------------------ 1 file changed, 1 insertion(+), 104 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index e092b07222c..dd6d647abc9 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -26,9 +26,6 @@ class Issue(Document): self.set_lead_contact(self.raised_by) - if not self.service_level_agreement: - self.reset_sla_fields() - def on_update(self): # Add a communication in the issue timeline if self.flags.create_communication and self.via_customer_portal: @@ -54,106 +51,6 @@ class Issue(Document): self.company = frappe.db.get_value("Lead", self.lead, "company") or \ frappe.db.get_default("Company") - def reset_sla_fields(self): - self.agreement_status = "" - self.response_by = "" - self.resolution_by = "" - self.response_by_variance = 0 - self.resolution_by_variance = 0 - - def update_status(self): - status = frappe.db.get_value("Issue", self.name, "status") - if self.status != "Open" and status == "Open" and not self.first_responded_on: - self.first_responded_on = frappe.flags.current_time or now_datetime() - - if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: - self.resolution_date = frappe.flags.current_time or now_datetime() - if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing": - set_service_level_agreement_variance(issue=self.name) - self.update_agreement_status() - set_resolution_time(issue=self) - set_user_resolution_time(issue=self) - - if self.status == "Open" and status != "Open": - # if no date, it should be set as None and not a blank string "", as per mysql strict config - self.resolution_date = None - self.reset_issue_metrics() - # enable SLA and variance on Reopen - self.agreement_status = "Ongoing" - set_service_level_agreement_variance(issue=self.name) - - self.handle_hold_time(status) - - def handle_hold_time(self, status): - if self.service_level_agreement: - # set response and resolution variance as None as the issue is on Hold - pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"], - filters={"parent": self.service_level_agreement}) - hold_statuses = [entry.status for entry in pause_sla_on] - update_values = {} - - if hold_statuses: - if self.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() - if not self.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - - # calculate hold time when status is changed from any hold status to any non-hold status - if self.status not in hold_statuses and status in hold_statuses: - hold_time = self.total_hold_time if self.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime() - last_hold_time = 0 - if self.on_hold_since: - # last_hold_time will be added to the sla variables - last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time - - # re-calculate SLA variables after issue changes from any hold status to any non-hold status - # add hold time to SLA variables - start_date_time = get_datetime(self.service_level_agreement_creation) - priority = get_priority(self) - now_time = frappe.flags.current_time or now_datetime() - - if not self.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time - - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - update_values['on_hold_since'] = None - - self.db_set(update_values) - - def update_agreement_status(self): - if self.service_level_agreement and self.agreement_status == "Ongoing": - if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0: - - self.agreement_status = "Failed" - else: - self.agreement_status = "Fulfilled" - - def update_agreement_status_on_custom_status(self): - """ - Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status - """ - if not self.first_responded_on: # first_responded_on set when first reply is sent to customer - self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2) - - if not self.resolution_date: # resolution_date set when issue has been closed - self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2) - - self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" - def create_communication(self): communication = frappe.new_doc("Communication") communication.update({ @@ -318,4 +215,4 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals def get_holidays(holiday_list_name): holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) holidays = [holiday.holiday_date for holiday in holiday_list.holidays] - return holidays + return holidays \ No newline at end of file From 86f41839fe820bb775438c27a354f56361c2404e Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:19:24 +0530 Subject: [PATCH 314/376] fix: Added a message to enable appointment booking if disabled (#26233) * fix: Added a message to enable appointment booking if disabled * refactor: added translation for the message Co-authored-by: Rucha Mahabal * fix: added missing import * fix: minor identation and space fix Co-authored-by: Rucha Mahabal --- erpnext/www/book_appointment/index.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 7bfac89f308..4f455614ba1 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -2,7 +2,7 @@ import frappe import datetime import json import pytz - +from frappe import _ WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -14,7 +14,8 @@ def get_context(context): if is_enabled: return context else: - frappe.local.flags.redirect_location = '/404' + frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"), + http_status_code=302, indicator_color="red") raise frappe.Redirect @frappe.whitelist(allow_guest=True) @@ -146,4 +147,4 @@ def _deltatime_to_datetime(date, deltatime): def _datetime_to_deltatime(date_time): midnight = datetime.datetime.combine(date_time.date(), datetime.time.min) - return (date_time-midnight) \ No newline at end of file + return (date_time-midnight) From 6c2f66b0a410337374d7c8ab4a3aad01ab34c63a Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:21:44 +0530 Subject: [PATCH 315/376] fix: Added permission for employee to book appointment (#26266) --- erpnext/crm/doctype/appointment/appointment.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 8517ddec321..016b8ec3e43 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -102,7 +102,7 @@ } ], "links": [], - "modified": "2020-01-28 16:16:45.447213", + "modified": "2021-06-30 12:09:14.228756", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -153,6 +153,18 @@ "role": "Sales User", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 } ], "quick_entry": 1, From 0a15a03522eb88bcec5a0e478dbfb9d180f8ee7f Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 2 Jul 2021 13:06:56 +0530 Subject: [PATCH 316/376] fix: lms progress issue (#26250) Co-authored-by: Rucha Mahabal --- erpnext/education/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py index 9db8a4a90df..3070e6a3e8a 100644 --- a/erpnext/education/utils.py +++ b/erpnext/education/utils.py @@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program): student = get_current_student() course_enrollment = get_enrollment("course", course, student.name) if not course_enrollment: - program_enrollment = get_enrollment('program', program, student.name) + program_enrollment = get_enrollment('program', program.name, student.name) if not program_enrollment: frappe.throw(_("You are not enrolled in program {0}").format(program)) return - return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name)) + return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name)) else: return frappe.get_doc('Course Enrollment', course_enrollment) From 4e6805b04ef48f47954f703e10dc95f525184541 Mon Sep 17 00:00:00 2001 From: Ashish Shah Date: Fri, 2 Jul 2021 13:35:04 +0530 Subject: [PATCH 318/376] fix: When Lead is created with mobile_no, mobile_no value gets lost (it is overwritten by phon value) (#26116) Steps to reproduce [1]Create a Lead. [2]Enter Person Name(lead_name): XX under Contact section, enter Phone(phone): 11 and Mobile No.(mobile_no):22 [3]Save it [4] F12, cur_frm.doc.phone : 11 (correct) cur_frm.doc.mobile_no : 11 (incorrect) [5]Under Address & Contact section ,check contact_html it shows ty Phone: 22 (correct) Phone: 11 (in correct) Actual: mobile_no value is lost. it is overwritten by phone value Expected: mobile_no value should be retained --- erpnext/crm/doctype/lead/lead.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index d1d096843bf..ce3de40fc3d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -168,12 +168,13 @@ class Lead(SellingController): if self.phone: contact.append("phone_nos", { "phone": self.phone, - "is_primary": 1 + "is_primary_phone": 1 }) if self.mobile_no: contact.append("phone_nos", { - "phone": self.mobile_no + "phone": self.mobile_no, + "is_primary_mobile_no":1 }) contact.insert(ignore_permissions=True) From 20f73d4c582766c0698370015340848f3d68fa32 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 2 Jul 2021 15:34:26 +0530 Subject: [PATCH 319/376] fix: only "Tax" type accounts should be shown for selection in GST Settings --- erpnext/regional/doctype/gst_settings/gst_settings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.js b/erpnext/regional/doctype/gst_settings/gst_settings.js index 808f9bc0789..cd682c5403e 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.js +++ b/erpnext/regional/doctype/gst_settings/gst_settings.js @@ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', { return { filters: { company: row.company, + account_type: "Tax", is_group: 0 } }; From 0a79cfa17044ec0e8f8ccbb31107ed39fa52edc0 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 2 Jul 2021 17:54:43 +0530 Subject: [PATCH 320/376] fix: set query for training events (#26302) * fix: set query * fix: whitespace between function name and param Co-authored-by: Rucha Mahabal --- .../hr/doctype/training_event/training_event.js | 14 ++++++++++---- .../training_event_employee.json | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index b7d34b178a0..064dfb24557 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', { frappe.set_route("List", "Training Feedback"); }); } - } -}); + frm.events.set_employee_query(frm); + }, -frappe.ui.form.on("Training Event Employee", { - employee: function (frm) { + set_employee_query: function(frm) { let emp = []; for (let d in frm.doc.employees) { if (frm.doc.employees[d].employee) { @@ -40,3 +39,10 @@ frappe.ui.form.on("Training Event Employee", { }); } }); + +frappe.ui.form.on("Training Event Employee", { + employee: function(frm) { + frm.events.set_employee_query(frm); + } +}); + diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json index 2d313e9faca..bcb7d5e5bc0 100644 --- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json +++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json @@ -19,6 +19,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Employee", + "no_copy": 1, "options": "Employee" }, { @@ -68,7 +69,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-21 12:41:59.336237", + "modified": "2021-07-02 17:20:27.630176", "modified_by": "Administrator", "module": "HR", "name": "Training Event Employee", From 802e63a9d77af37abb29c8a28e093d0a08a89612 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Jun 2021 11:24:32 +0530 Subject: [PATCH 321/376] fix: Do not consider cancelled entries in party dashboard --- erpnext/accounts/party.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e025fc69054..b97dc401e6a 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where party_type = %s and party=%s + and is_cancelled = 0 group by company""", (party_type, party))) for d in companies: From 5069095984270bd7599fce078444557489523e84 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 09:31:31 +0530 Subject: [PATCH 322/376] fix: Auto process deferred accounting for multi-company setup --- erpnext/accounts/deferred_revenue.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 2f86c6c1de2..335e8a15ab0 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None): start_date = add_months(today(), -1) end_date = add_days(today(), -1) - for record_type in ('Income', 'Expense'): - doc = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=posting_date, - start_date=start_date, - end_date=end_date, - type=record_type - )) + companies = frappe.get_all('Company') - doc.insert() - doc.submit() + for company in companies: + for record_type in ('Income', 'Expense'): + doc = frappe.get_doc(dict( + doctype='Process Deferred Accounting', + company=company.name, + posting_date=posting_date, + start_date=start_date, + end_date=end_date, + type=record_type + )) + + doc.insert() + doc.submit() def make_gl_entries(doc, credit_account, debit_account, against, amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): From 9d295ca93972679773f1de806a2992819250a1c0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 18:56:51 +0530 Subject: [PATCH 323/376] fix: Bank statement import --- .../doctype/bank_statement_import/bank_statement_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 5f110e2727c..ffc9d1c4658 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -51,7 +51,7 @@ class BankStatementImport(DataImport): self.import_file, self.google_sheets_url ) - if 'Bank Account' not in json.dumps(preview): + if 'Bank Account' not in json.dumps(preview['columns']): frappe.throw(_("Please add the Bank Account column")) from frappe.core.page.background_jobs.background_jobs import get_info From 046e83bf5040d690b546d0499099baf80fe6ad68 Mon Sep 17 00:00:00 2001 From: Richard Case <64409021+casesolved-co-uk@users.noreply.github.com> Date: Mon, 5 Jul 2021 06:26:34 +0100 Subject: [PATCH 324/376] fix: incorrect bom no. added for non-variant items on variant boms (#26320) --- erpnext/manufacturing/doctype/bom/bom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c58f017258e..3bd1fe6c7fe 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1100,6 +1100,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): }, 'BOM Item': { 'doctype': 'BOM Item', + # stop get_mapped_doc copying parent bom_no to children + 'field_no_map': ['bom_no'], 'condition': lambda doc: doc.has_variants == 0 }, }, target_doc, postprocess) From 03f7bf6589b9d0df64368339a79ad90807497bb2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 5 Jul 2021 11:48:36 +0530 Subject: [PATCH 325/376] test: variant BOM from template BOM --- erpnext/manufacturing/doctype/bom/test_bom.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 57a54587269..c89f7d66fdf 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -8,6 +8,7 @@ import frappe from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation +from erpnext.manufacturing.doctype.bom.bom import make_variant_bom from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -248,6 +249,37 @@ class TestBOM(unittest.TestCase): for reqd_item, created_item in zip(reqd_order, created_order): self.assertEqual(reqd_item, created_item.item_code) + def test_generated_variant_bom(self): + from erpnext.controllers.item_variant import create_variant + + template_item = make_item( + "_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]} + ) + variant = create_variant(template_item.item_code, {"Test Size": "Large"}) + variant.insert(ignore_if_duplicate=True) + + bom_tree = { + template_item.item_code: { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "ChildPart5": {}, + } + } + template_bom = create_nested_bom(bom_tree, prefix="") + variant_bom = make_variant_bom( + template_bom.name, template_bom.name, variant.item_code, variant_items=[] + ) + variant_bom.save() + + reqd_order = template_bom.get_tree_representation().level_order_traversal() + created_order = variant_bom.get_tree_representation().level_order_traversal() + + self.assertEqual(len(reqd_order), len(created_order)) + + for reqd_item, created_item in zip(reqd_order, created_order): + self.assertEqual(reqd_item.item_code, created_item.item_code) + self.assertEqual(reqd_item.qty, created_item.qty) + self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From 8c1b764a90ed543df6d350e1184128354f5f6311 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 21:49:10 +0530 Subject: [PATCH 326/376] fix: Debug tests --- .../doctype/purchase_invoice/test_purchase_invoice.py | 1 + .../test_service_level_agreement.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ec93314c0f6..2ce65c13bfd 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1042,6 +1042,7 @@ class TestPurchaseInvoice(unittest.TestCase): where voucher_type='Purchase Invoice' and voucher_no=%s order by account asc""", (purchase_invoice.name), as_dict=1) + print(gl_entries) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.debit) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 2a8446d29f9..4ef49306594 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -81,6 +81,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA custom fields created for leads sla_fields = get_service_level_agreement_fields() + frappe.clear_cache(doctype=doctype) meta = frappe.get_meta(doctype, cached=False) for field in sla_fields: @@ -219,9 +220,9 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') - def tearDown(self): - for d in frappe.get_all("Service Level Agreement"): - frappe.delete_doc("Service Level Agreement", d.name, force=1) + # def tearDown(self): + # for d in frappe.get_all("Service Level Agreement"): + # frappe.delete_doc("Service Level Agreement", d.name, force=1) def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue"): From 8418c4bfe00b926d0943194a16529150aae19cc6 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 22 Jun 2021 21:35:25 +0530 Subject: [PATCH 327/376] fix: Include Stock Reco logic in update_qty_in_future_sle --- erpnext/stock/stock_ledger.py | 75 ++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9fe89c3fa59..94bd3077a73 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -215,7 +215,7 @@ class update_entries_after(object): """ self.data.setdefault(args.warehouse, frappe._dict()) warehouse_dict = self.data[args.warehouse] - previous_sle = self.get_previous_sle_of_current_voucher(args) + previous_sle = get_previous_sle_of_current_voucher(args) warehouse_dict.previous_sle = previous_sle for key in ("qty_after_transaction", "valuation_rate", "stock_value"): @@ -227,29 +227,6 @@ class update_entries_after(object): "stock_value_difference": 0.0 }) - def get_previous_sle_of_current_voucher(self, args): - """get stock ledger entries filtered by specific posting datetime conditions""" - - args['time_format'] = '%H:%i:%s' - if not args.get("posting_date"): - args["posting_date"] = "1900-01-01" - if not args.get("posting_time"): - args["posting_time"] = "00:00" - - sle = frappe.db.sql(""" - select *, timestamp(posting_date, posting_time) as "timestamp" - from `tabStock Ledger Entry` - where item_code = %(item_code)s - and warehouse = %(warehouse)s - and is_cancelled = 0 - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) - order by timestamp(posting_date, posting_time) desc, creation desc - limit 1 - for update""", args, as_dict=1) - - return sle[0] if sle else frappe._dict() - - def build(self): from erpnext.controllers.stock_controller import future_sle_exists @@ -734,6 +711,35 @@ class update_entries_after(object): bin_doc.flags.via_stock_ledger_entry = True bin_doc.save(ignore_permissions=True) + +def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): + """get stock ledger entries filtered by specific posting datetime conditions""" + + args['time_format'] = '%H:%i:%s' + if not args.get("posting_date"): + args["posting_date"] = "1900-01-01" + if not args.get("posting_time"): + args["posting_time"] = "00:00" + + voucher_condition = "" + if exclude_current_voucher: + voucher_no = args.get("voucher_no") + voucher_condition = f"and voucher_no != '{voucher_no}'" + + sle = frappe.db.sql(""" + select *, timestamp(posting_date, posting_time) as "timestamp" + from `tabStock Ledger Entry` + where item_code = %(item_code)s + and warehouse = %(warehouse)s + and is_cancelled = 0 + {voucher_condition} + and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + order by timestamp(posting_date, posting_time) desc, creation desc + limit 1 + for update""".format(voucher_condition=voucher_condition), args, as_dict=1) + + return sle[0] if sle else frappe._dict() + def get_previous_sle(args, for_update=False): """ get the last sle on or before the current time-bucket, @@ -862,9 +868,24 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate def update_qty_in_future_sle(args, allow_negative_stock=None): + """Recalculate Qty after Transaction in future SLEs based on current SLE.""" + qty_shift = args.actual_qty + + # find difference/shift in qty caused by stock reconciliation + if args.voucher_type == "Stock Reconciliation": + last_balance = get_previous_sle_of_current_voucher( + args, + exclude_current_voucher=True + ).get("qty_after_transaction") + if last_balance is not None: + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = args.qty_after_transaction + qty_shift = stock_reco_qty_shift + frappe.db.sql(""" update `tabStock Ledger Entry` - set qty_after_transaction = qty_after_transaction + {qty} + set qty_after_transaction = qty_after_transaction + {qty_shift} where item_code = %(item_code)s and warehouse = %(warehouse)s @@ -876,7 +897,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): and creation > %(creation)s ) ) - """.format(qty=args.actual_qty), args) + """.format(qty_shift=qty_shift), args) validate_negative_qty_in_future_sle(args, allow_negative_stock) @@ -884,7 +905,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): allow_negative_stock = allow_negative_stock \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if args.actual_qty < 0 and not allow_negative_stock: + if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: sle = get_future_sle_with_negative_qty(args) if sle: message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( From 4038977e2ed3f5e33b8c7151896c79ed3043981b Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Jul 2021 17:13:45 +0530 Subject: [PATCH 328/376] fix: Handle Stock Reco cancellation and limit reposting - Handled cancellation of reco with and without prior SLE - Repost / Recalculate balance qty only till next stock reco --- .../stock_reconciliation.py | 1 + erpnext/stock/stock_ledger.py | 84 ++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 2956384a672..3e15d547e03 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -357,6 +357,7 @@ class StockReconciliation(StockController): if row.current_qty: data.actual_qty = -1 * row.current_qty data.qty_after_transaction = flt(row.current_qty) + data.previous_qty_after_transaction = flt(row.qty) data.valuation_rate = flt(row.current_valuation_rate) data.stock_value = data.qty_after_transaction * data.valuation_rate data.stock_value_difference = -1 * flt(row.amount_difference) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 94bd3077a73..7425473f9d0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -55,6 +55,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() + + if sle.get("voucher_type") == "Stock Reconciliation": + # preserve previous_qty_after_transaction for qty reposting + args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") + update_bin(args, allow_negative_stock, via_landed_cost_voucher) def get_args_for_future_sle(row): @@ -869,19 +874,21 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, def update_qty_in_future_sle(args, allow_negative_stock=None): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" + datetime_limit_condition = "" + last_balance = None + qty_shift = args.actual_qty # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": - last_balance = get_previous_sle_of_current_voucher( - args, - exclude_current_voucher=True - ).get("qty_after_transaction") - if last_balance is not None: - stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) - else: - stock_reco_qty_shift = args.qty_after_transaction - qty_shift = stock_reco_qty_shift + qty_shift = get_stock_reco_qty_shift(args) + + # find the next nearest stock reco so that we only recalculate SLEs till that point + next_stock_reco_detail = get_next_stock_reco(args) + if next_stock_reco_detail: + detail = next_stock_reco_detail[0] + # add condition to update SLEs before this date & time + datetime_limit_condition = get_datetime_limit_condition(detail) frappe.db.sql(""" update `tabStock Ledger Entry` @@ -897,10 +904,67 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): and creation > %(creation)s ) ) - """.format(qty_shift=qty_shift), args) + {datetime_limit_condition} + """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args) validate_negative_qty_in_future_sle(args, allow_negative_stock) +def get_stock_reco_qty_shift(args): + stock_reco_qty_shift = 0 + if args.get("is_cancelled"): + if args.get("previous_qty_after_transaction"): + # get qty (balance) that was set at submission + last_balance = args.get("previous_qty_after_transaction") + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = flt(args.actual_qty) + else: + # reco is being submitted + last_balance = get_previous_sle_of_current_voucher(args, + exclude_current_voucher=True).get("qty_after_transaction") + + if last_balance is not None: + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = args.qty_after_transaction + + return stock_reco_qty_shift + +def get_next_stock_reco(args): + """Returns next nearest stock reconciliaton's details.""" + + return frappe.db.sql(""" + select + name, posting_date, posting_time, creation, voucher_no + from + `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_type = 'Stock Reconciliation' + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) + or ( + timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) + and creation > %(creation)s + ) + ) + limit 1 + """, args, as_dict=1) + +def get_datetime_limit_condition(detail): + if not detail: return None + + return f""" + and + (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') + or ( + timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}') + and creation < '{detail.creation}' + ) + )""" + def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): allow_negative_stock = allow_negative_stock \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) From 8c441263f81c1581bcecbf84c091e4b68c3cf153 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Jul 2021 17:46:05 +0530 Subject: [PATCH 329/376] fix: Sider --- erpnext/stock/stock_ledger.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7425473f9d0..4e9c7689ae4 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -875,8 +875,6 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, def update_qty_in_future_sle(args, allow_negative_stock=None): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" - last_balance = None - qty_shift = args.actual_qty # find difference/shift in qty caused by stock reconciliation @@ -937,7 +935,7 @@ def get_next_stock_reco(args): select name, posting_date, posting_time, creation, voucher_no from - `tabStock Ledger Entry` + `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s @@ -954,8 +952,6 @@ def get_next_stock_reco(args): """, args, as_dict=1) def get_datetime_limit_condition(detail): - if not detail: return None - return f""" and (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') From e8c9ab4b01ff86eb72a9ff536f4f7d05b0dd8313 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 20:23:00 +0530 Subject: [PATCH 330/376] chore: Test for backdated reco qty reposting --- .../test_stock_reconciliation.py | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 36380b838b1..f7b243221a9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, unittest -from frappe.utils import flt, nowdate, nowtime +from frappe.utils import flt, nowdate, nowtime, add_days from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items @@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt class TestStockReconciliation(unittest.TestCase): @classmethod @@ -204,6 +205,74 @@ class TestStockReconciliation(unittest.TestCase): self.assertEqual(sr.get("items")[0].valuation_rate, 0) self.assertEqual(sr.get("items")[0].amount, 0) + def test_backdated_stock_reco_qty_reposting(self): + """ + Test if a backdated stock reco recalculates future qty until next reco. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] + PR1 | PR | 10 | 18 (posting date: today-3) + PR2 | PR | 1 | 19 (posting date: today-2) + SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] + PR3 | PR | 1 | 7 (posting date: today) # can't post future PR + """ + item_code = "Backdated-Reco-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, + posting_date=add_days(nowdate(), -3)) + pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=add_days(nowdate(), -2)) + pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=nowdate()) + + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(pr3_balance, 12) + + # post backdated stock reco in between + sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100, + posting_date=add_days(nowdate(), -1)) + pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr3_balance, 7) + + # post backdated stock reco at the start + sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100, + posting_date=add_days(nowdate(), -4)) + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, + "qty_after_transaction") + sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 18) + self.assertEqual(pr2_balance, 19) + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + + # cancel backdated stock reco and check future impact + sr5.cancel() + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, + "qty_after_transaction") + sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(pr2_balance, 11) + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + + # teardown + sr4.cancel() + pr3.cancel() + pr2.cancel() + pr1.cancel() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From 44c1e8da06e1c725f715120e15d6500ca0044eca Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 21:56:03 +0530 Subject: [PATCH 331/376] chore: Test to block backdated reco causing future scarcity --- .../test_stock_reconciliation.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index f7b243221a9..7b98c7b3e2c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -273,6 +273,49 @@ class TestStockReconciliation(unittest.TestCase): pr2.cancel() pr1.cancel() + def test_backdated_stock_reco_future_negative_stock(self): + """ + Test if a backdated stock reco causes future negative stock and is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + PR1 | PR | 10 | 10 (posting date: today-2) + SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] + DN2 | DN | -2 | 8(-1) (posting date: today) + """ + from erpnext.stock.stock_ledger import NegativeStockError + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = "Backdated-Reco-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0) + + pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, + posting_date=add_days(nowdate(), -2)) + dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120, + posting_date=nowdate()) + + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(dn2_balance, 8) + + # check if stock reco is blocked + sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=add_days(nowdate(), -1), do_not_submit=True) + self.assertRaises(NegativeStockError, sr3.submit) + + # teardown + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting) + sr3.cancel() + dn2.cancel() + pr1.cancel() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From c21d3d486597a3a88a98fb34c8616bb6117a6499 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 22:47:59 +0530 Subject: [PATCH 332/376] fix: Debug tests --- .../service_level_agreement/test_service_level_agreement.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4ef49306594..61666f142a3 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -103,6 +103,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() + frappe.clear_cache(doctype=doctype) meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: From 32436eceda9d976692aa6a48f75738d77a00af9a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 23:34:09 +0530 Subject: [PATCH 333/376] fix: Debug tests --- .../service_level_agreement/test_service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 61666f142a3..e5076193e28 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -103,7 +103,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() - frappe.clear_cache(doctype=doctype) + frappe.clear_cache(doctype=doctype.doctype) meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: From 66c04ca9841a6b30e602a2a29dfd0fa45f3ea3e6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Jul 2021 10:56:42 +0530 Subject: [PATCH 334/376] fix: Debug tests --- .../service_level_agreement/test_service_level_agreement.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index e5076193e28..4ef49306594 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -103,7 +103,6 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() - frappe.clear_cache(doctype=doctype.doctype) meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: From 96eb3be6df20c4d3ac38066ed12e3c1e7d133e3a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Jul 2021 11:21:44 +0530 Subject: [PATCH 335/376] fix: Debug Tests --- .../service_level_agreement/test_service_level_agreement.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4ef49306594..a5481b1123e 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -220,9 +220,9 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') - # def tearDown(self): - # for d in frappe.get_all("Service Level Agreement"): - # frappe.delete_doc("Service Level Agreement", d.name, force=1) + def tearDown(self): + for d in frappe.get_all("Service Level Agreement"): + frappe.delete_doc("Service Level Agreement", d.name, force=1) def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue"): From f67c95d32feb9e88218fae003b21471dc5c4662e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 6 Jul 2021 13:27:48 +0530 Subject: [PATCH 336/376] fix: flaky SLA test --- .../service_level_agreement/test_service_level_agreement.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 2a8446d29f9..0d20b98fa70 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -81,10 +81,9 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA custom fields created for leads sla_fields = get_service_level_agreement_fields() - meta = frappe.get_meta(doctype, cached=False) for field in sla_fields: - self.assertTrue(meta.has_field(field.get("fieldname"))) + self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")})) def test_docfield_creation_for_sla_on_custom_dt(self): doctype = create_custom_doctype() @@ -102,10 +101,9 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() - meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: - self.assertTrue(meta.has_field(field.get("fieldname"))) + self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name})) def test_sla_application(self): # Default Service Level Agreement From 1f5e2ba8e773ccb8ee37f0ec28096604fc8b3f31 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 6 Jul 2021 13:36:23 +0530 Subject: [PATCH 337/376] fix: payroll-entry minor fix --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 36e728fc992..388a44d895f 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -686,7 +686,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) - emp = filters.get('employees') + emp = filters.get('employees') or [] include_employees = [employee.employee for employee in employee_list if employee.employee not in emp] filters.pop('start_date') filters.pop('end_date') From 53e3435770de83ad6d66b00270d23a9eeaaa91a1 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 6 Jul 2021 14:37:21 +0530 Subject: [PATCH 338/376] fix: remove cancelled entries in consolidated financial statements (#26330) --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 7793af737f9..56a67bb0989 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, acc.account_name, acc.account_number - from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s + from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions), { From 8985231a96a967683c3c3d3baad66aedb44757ca Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Jul 2021 15:34:47 +0530 Subject: [PATCH 339/376] fix: Rewrite tests --- .../purchase_invoice/test_purchase_invoice.py | 37 +++++++++---------- .../purchase_receipt/test_purchase_receipt.py | 32 ++++++++-------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2ce65c13bfd..189260a29da 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -231,25 +231,25 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_purchase_invoice_with_exchange_rate_difference(self): - pr = make_purchase_receipt(currency = "USD", conversion_rate = 70) - pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True") + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice - pi.items[0].purchase_receipt = pr.name - pi.items[0].pr_detail = pr.items[0].name + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', + currency = "USD", conversion_rate = 70) + + pi = create_purchase_invoice(pr.name) + pi.conversion_rate = 80 pi.insert() pi.submit() - # fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account - gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'}) - voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') + # Get exchnage gain and loss account + exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account') - self.assertEqual(pi.name, voucher_no) - - exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + # fetching the latest GL Entry with exchange gain and loss account account + amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit') discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) - self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) @@ -1031,22 +1031,21 @@ class TestPurchaseInvoice(unittest.TestCase): # Check GLE for Purchase Invoice # Zero net effect on final TDS Payable on invoice expected_gle = [ - ['_Test Account Cost for Goods Sold - _TC', 30000, 0], - ['_Test Account Excise Duty - _TC', 0, 3000], - ['Creditors - _TC', 0, 27000], - ['TDS Payable - _TC', 3000, 3000] + ['_Test Account Cost for Goods Sold - _TC', 30000], + ['_Test Account Excise Duty - _TC', -3000], + ['Creditors - _TC', -27000], + ['TDS Payable - _TC', 0] ] - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s + group by account order by account asc""", (purchase_invoice.name), as_dict=1) - print(gl_entries) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) - self.assertEqual(expected_gle[i][1], gle.debit) - self.assertEqual(expected_gle[i][2], gle.credit) + self.assertEqual(expected_gle[i][1], gle.amount) def update_tax_witholding_category(company, account, date): from erpnext.accounts.utils import get_fiscal_year diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d56822a3081..dbba21fde1b 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1054,30 +1054,30 @@ class TestPurchaseReceipt(unittest.TestCase): def test_purchase_receipt_with_exchange_rate_difference(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice - - pi = create_purchase_invoice(currency = "USD", conversion_rate = 70) - - create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", - properties={"account": '_Test Account Stock In Hand - TCP1'}) + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt - pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', - company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80, - do_not_save = "True") - + pi = create_purchase_invoice(company="_Test Company with perpetual inventory", + cost_center = "Main - TCP1", + warehouse = "Stores - TCP1", + expense_account ="_Test Account Cost for Goods Sold - TCP1", + currency = "USD", conversion_rate = 70) + + pr = create_purchase_receipt(pi.name) + pr.conversion_rate = 80 pr.items[0].purchase_invoice = pi.name pr.items[0].purchase_invoice_item = pi.items[0].name - pr.insert() + pr.save() pr.submit() - # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account - gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'}) - voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') - self.assertEqual(pr.name, voucher_no) + # Get exchnage gain and loss account + exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account') - exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + # fetching the latest GL Entry with exchange gain and loss account account + amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit') discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) - self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + + self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference From 0734901a89d01c4358fa579517a572705a360dc4 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 6 Jul 2021 15:56:10 +0530 Subject: [PATCH 340/376] fix: stock_rbnb not defined (#26345) --- .../stock/doctype/purchase_receipt/purchase_receipt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 5ba9c7057bb..41800e37151 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -291,7 +291,7 @@ class PurchaseReceipt(BuyingController): continue self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, - stock_rbnb, account_currency=warehouse_account_currency, item=d) + stock_rbnb, account_currency=warehouse_account_currency, item=d) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation @@ -318,11 +318,11 @@ class PurchaseReceipt(BuyingController): (exchange_rate_map[d.purchase_invoice] - self.conversion_rate) self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=credit_currency, item=d) - self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=credit_currency, item=d) # Amount added through landed-cos-voucher @@ -407,6 +407,7 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked + stock_rbnb = self.get_company_default("stock_received_but_not_billed") i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): From f9e9d86955f20c58f02abf8f3c85c19c014ee28f Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 6 Jul 2021 18:09:21 +0530 Subject: [PATCH 341/376] test: fetching of previous sle (#26352) --- .../test_stock_ledger_entry.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index ba31ad7b06b..af2ada8c9a4 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -54,7 +54,7 @@ class TestStockLedgerEntry(unittest.TestCase): ) # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020 - make_stock_entry( + se = make_stock_entry( item_code="_Test Item for Reposting", source="Stores - _TC", target="Finished Goods - _TC", @@ -64,29 +64,29 @@ class TestStockLedgerEntry(unittest.TestCase): posting_date='2020-04-30', posting_time='14:00' ) - target_wh_sle = get_previous_sle({ + target_wh_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-04-30', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": se.name + }, ["valuation_rate"], as_dict=1) self.assertEqual(target_wh_sle.get("valuation_rate"), 150) # Repack entry on 5-5-2020 repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00') - finished_item_sle = get_previous_sle({ + finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Finished Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-05-05', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": repack.name + }, ["incoming_rate", "valuation_rate"], as_dict=1) self.assertEqual(finished_item_sle.get("incoming_rate"), 540) self.assertEqual(finished_item_sle.get("valuation_rate"), 540) # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150 - create_stock_reconciliation( + sr = create_stock_reconciliation( item_code="_Test Item for Reposting", warehouse="Stores - _TC", qty=50, @@ -109,12 +109,12 @@ class TestStockLedgerEntry(unittest.TestCase): self.assertEqual(target_wh_sle.get("valuation_rate"), 175) # Check valuation rate of repacked item after back-dated entry at Stores - finished_item_sle = get_previous_sle({ + finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Finished Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-05-05', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": repack.name + }, ["incoming_rate", "valuation_rate"], as_dict=1) self.assertEqual(finished_item_sle.get("incoming_rate"), 790) self.assertEqual(finished_item_sle.get("valuation_rate"), 790) From dd1b2995a88a7379af52d6a3d27d4f676ebc6777 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Jul 2021 14:24:42 +0530 Subject: [PATCH 342/376] fix: stock entry with putaway rule not working --- erpnext/stock/doctype/putaway_rule/putaway_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index ea26caced04..0f50bcd6ea8 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -97,7 +97,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: - warehouse = source_warehouse or item.warehouse + warehouse = source_warehouse or item.get('warehouse') if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) From 290350c86f3fdb5845312986d9c55c1d62c0939c Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 7 Jul 2021 12:10:02 +0530 Subject: [PATCH 343/376] chore: add product listing link in settings (#26026) * chore: add product listing link in settings * chore: add icon in workspace card Co-authored-by: Ankush --- .../workspace/erpnext_settings/erpnext_settings.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 014f4095c15..6ca3d637da4 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -11,10 +11,11 @@ "hide_custom": 0, "icon": "settings", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "ERPNext Settings", "links": [], - "modified": "2020-12-01 13:38:37.759596", + "modified": "2021-06-12 01:58:11.399566", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -109,6 +110,13 @@ "label": "Domain Settings", "link_to": "Domain Settings", "type": "DocType" + }, + { + "doc_view": "", + "icon": "retail", + "label": "Products Settings", + "link_to": "Products Settings", + "type": "DocType" } ] -} \ No newline at end of file +} From eaef371585324c619e4f2a3a25f245409a779fee Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 8 Jul 2021 10:52:41 +0530 Subject: [PATCH 344/376] fix: escape quotes while fetching customer emails (#26329) --- .../process_statement_of_accounts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 0b0ee904ff9..500952e38ad 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): @frappe.whitelist() def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): billing_email = frappe.db.sql(""" - SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ - WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ - c.is_billing_contact=1 \ - order by c.creation desc""") + SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent + WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1 + order by c.creation desc""", customer_name) if len(billing_email) == 0 or (billing_email[0][0] is None): if billing_and_primary: From fac420ee09d0e7870e9ecd98a21628b095497c41 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 8 Jul 2021 13:05:14 +0530 Subject: [PATCH 345/376] fix: Removed un-used flag --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 388a44d895f..13cc423fc2c 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -117,7 +117,6 @@ class PayrollEntry(Document): Creates salary slip for selected employees if already not created """ self.check_permission('write') - self.created = 1 employees = [emp.employee for emp in self.employees] if employees: args = frappe._dict({ From 091f41e98657839c565d8825a3ffa79659c81d89 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 14:57:54 +0530 Subject: [PATCH 346/376] fix: yet another fix for flaky SLA Test --- .../service_level_agreement.json | 5 +++-- .../test_service_level_agreement.py | 13 ++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 61ca3a334e4..de3389aa422 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -150,7 +150,8 @@ "fieldtype": "Link", "label": "Document Type", "options": "DocType", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "default": "1", @@ -178,7 +179,7 @@ } ], "links": [], - "modified": "2021-05-29 13:35:41.956849", + "modified": "2021-07-08 12:28:46.283334", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 0d20b98fa70..7c18a6577fa 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -328,16 +328,11 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "entity": entity }) - service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters) + sla = frappe.db.exists("Service Level Agreement", filters) + if sla: + frappe.delete_doc("Service Level Agreement", sla, force=1) - if not service_level_agreement_exists: - doc = frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) - else: - doc = frappe.get_doc("Service Level Agreement", service_level_agreement_exists) - doc.update(service_level_agreement) - doc.save() - - return doc + return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) def create_customer(): From b6ff8917e819e8fb272f4cd91e4ee01217027889 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 8 Jul 2021 17:25:37 +0530 Subject: [PATCH 347/376] fix: query for training Event --- erpnext/hr/doctype/training_event/training_event.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index 064dfb24557..d5f6e5f573e 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -33,7 +33,8 @@ frappe.ui.form.on('Training Event', { frm.set_query("employee", "employees", function () { return { filters: { - name: ["NOT IN", emp] + name: ["NOT IN", emp], + status: "Active" } }; }); From 88929b055c3847b386f7ab8e708ee0a6bc1303ec Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 19:27:53 +0530 Subject: [PATCH 348/376] fix: precision for expected values in payment entry test --- .../accounts/doctype/payment_entry/test_payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 4641d6b5ffa..d1302f5ae78 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase): party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center) self.assertEqual(pe.cost_center, si.cost_center) - self.assertEqual(expected_account_balance, account_balance) - self.assertEqual(expected_party_balance, party_balance) - self.assertEqual(expected_party_account_balance, party_account_balance) + self.assertEqual(flt(expected_account_balance), account_balance) + self.assertEqual(flt(expected_party_balance), party_balance) + self.assertEqual(flt(expected_party_account_balance), party_account_balance) def create_payment_terms_template(): From 86f5dfbb8ffc7f1340c5d05ecc445b39b8d5892f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 14:24:38 +0530 Subject: [PATCH 349/376] fix: Validate LCV for Invoices without Update Stock --- .../landed_cost_voucher/landed_cost_voucher.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 5df4d8743fc..1f78867bef8 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -60,8 +60,19 @@ class LandedCostVoucher(Document): receipt_documents = [] for d in self.get("purchase_receipts"): - if frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") != 1: - frappe.throw(_("Receipt document must be submitted")) + doc_data = frappe.db.get_values( + d.receipt_document_type, + d.receipt_document, + ["docstatus", "update_stock"], + as_dict=1 + )[0] + if doc_data.get("docstatus") != 1: + msg = f"Row {d.idx}: Receipt Document {frappe.bold(d.receipt_document)} must be submitted" + frappe.throw(_(msg), title=_("Invalid Document")) + elif d.receipt_document_type == "Purchase Invoice" and not doc_data.get("update_stock"): + msg = _(f"Row {d.idx}: Purchase Invoice {frappe.bold(d.receipt_document)} has no stock impact.") + msg += "
    " + _("Please create Landed Cost Vouchers against Invoices with 'Update Stock' enabled.") + frappe.throw(msg, title=_("Incorrect Invoice")) else: receipt_documents.append(d.receipt_document) From 98173038b5fb7cfad22ee48febdca62d888c70c8 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 9 Jul 2021 13:06:38 +0530 Subject: [PATCH 350/376] fix: Rename function and tweak logic - Dont validate PI on `else` --- .../landed_cost_voucher.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) 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 1f78867bef8..bf969f99f8e 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -41,7 +41,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() - self.validate_purchase_receipts() + self.validate_receipt_documents() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -56,25 +56,23 @@ class LandedCostVoucher(Document): frappe.throw(_("Please enter Receipt Document")) - def validate_purchase_receipts(self): + def validate_receipt_documents(self): receipt_documents = [] for d in self.get("purchase_receipts"): - doc_data = frappe.db.get_values( - d.receipt_document_type, - d.receipt_document, - ["docstatus", "update_stock"], - as_dict=1 - )[0] - if doc_data.get("docstatus") != 1: - msg = f"Row {d.idx}: Receipt Document {frappe.bold(d.receipt_document)} must be submitted" + docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") + if docstatus != 1: + msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" frappe.throw(_(msg), title=_("Invalid Document")) - elif d.receipt_document_type == "Purchase Invoice" and not doc_data.get("update_stock"): - msg = _(f"Row {d.idx}: Purchase Invoice {frappe.bold(d.receipt_document)} has no stock impact.") - msg += "
    " + _("Please create Landed Cost Vouchers against Invoices with 'Update Stock' enabled.") - frappe.throw(msg, title=_("Incorrect Invoice")) - else: - receipt_documents.append(d.receipt_document) + + if d.receipt_document_type == "Purchase Invoice": + update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") + if not update_stock: + msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document)) + msg += "
    " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.") + frappe.throw(msg, title=_("Incorrect Invoice")) + + receipt_documents.append(d.receipt_document) for item in self.get("items"): if not item.receipt_document: From 99f449939df4dcef2ba135d9dfc58438fee00989 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Jul 2021 17:20:24 +0530 Subject: [PATCH 351/376] fix: Order Items by weightage in the web items query --- erpnext/shopping_cart/product_query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 3eab4ffbcc9..6c92d967d0c 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -87,7 +87,8 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, start=start, - limit=self.page_length + limit=self.page_length, + order_by="weightage desc" ) # Combine results having context of website item groups into item results From 95a15ed85d78f8506d65d6aad20600e489623b40 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:34:51 +0530 Subject: [PATCH 352/376] fix: value fetching for custom field in POS (#26366) --- erpnext/selling/page/point_of_sale/pos_payment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index c484873d3e4..f1a166b5230 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -56,7 +56,7 @@ erpnext.PointOfSale.Payment = class { ); let df_events = { onchange: function() { - frm.set_value(this.df.fieldname, this.value); + frm.set_value(this.df.fieldname, this.get_value()); } }; if (df.fieldtype == "Button") { From 47ccfd6c9ddca414ababbdb9222431426d14c806 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 14:36:13 +0530 Subject: [PATCH 353/376] fix: column 'outstanding_amount' cannot be null (#26371) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index adaf99a7900..0c21aae944c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1318,9 +1318,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre return frappe._dict({ "due_date": ref_doc.get("due_date"), - "total_amount": total_amount, - "outstanding_amount": outstanding_amount, - "exchange_rate": exchange_rate, + "total_amount": flt(total_amount), + "outstanding_amount": flt(outstanding_amount), + "exchange_rate": flt(exchange_rate), "bill_no": bill_no }) From b1997029775d599b6c1bac3b1ee83edef5edec64 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 14:37:34 +0530 Subject: [PATCH 354/376] fix(e-invoicing): allow export invoice even if no taxes applied (#26363) --- erpnext/regional/india/e_invoice/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5d33c1b100a..fad1c4636d5 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -42,7 +42,10 @@ def validate_eligibility(doc): invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') - no_taxes_applied = not doc.get('taxes') + + # if export invoice, then taxes can be empty + # invoice can only be ineligible if no taxes applied and is not an export invoice + no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: From 77e00403c82ea0d15caab65e81659dc10de73bed Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 14:41:03 +0530 Subject: [PATCH 355/376] fix: omit item discount amount for e-invoicing (#26353) --- erpnext/regional/india/e_invoice/einvoice.js | 4 +++- erpnext/regional/india/e_invoice/utils.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 23d4fe9030b..8ad30fa9106 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -1,6 +1,8 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.ui.form.on(doctype, { async refresh(frm) { + if (frm.doc.docstatus == 2) return; + const res = await frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', args: { doc: frm.doc } @@ -111,7 +113,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const action = () => { - let message = __('Cancellation of e-way bill is currently not supported. '); + let message = __('Cancellation of e-way bill is currently not supported.') + ' '; message += '

    '; message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index fad1c4636d5..81c7a6b9a07 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -191,9 +191,10 @@ def get_item_list(invoice): item.qty = abs(item.qty) - item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) - item.gross_amount = abs(item.taxable_value) + item.discount_amount + item.unit_rate = abs(item.taxable_value / item.qty) + item.gross_amount = abs(item.taxable_value) item.taxable_value = abs(item.taxable_value) + item.discount_amount = 0 item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None From 2098684bc084ab4ccd0db7cb08ffeb2399be30c5 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 18:09:26 +0530 Subject: [PATCH 356/376] fix(pos): taxes amount in pos item cart (#26410) --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 7cae0e47974..38508c219b3 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -472,12 +472,7 @@ erpnext.PointOfSale.ItemCart = class { const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); - const taxes = frm.doc.taxes.map(t => { - return { - description: t.description, rate: t.rate - }; - }); - this.render_taxes(frm.doc.total_taxes_and_charges, taxes); + this.render_taxes(frm.doc.taxes); } render_net_total(value) { @@ -502,14 +497,14 @@ erpnext.PointOfSale.ItemCart = class { ); } - render_taxes(value, taxes) { + render_taxes(taxes) { if (taxes.length) { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; return `
    ${description}
    -
    ${format_currency(value, currency)}
    +
    ${format_currency(t.tax_amount_after_discount_amount, currency)}
    `; }).join(''); this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); From 446171acc4232ec964473804e73ab6dfc413081d Mon Sep 17 00:00:00 2001 From: Anuja Date: Tue, 29 Jun 2021 21:27:02 +0530 Subject: [PATCH 357/376] fix: updated onboarding steps for selling module --- .../form_tour/sales_order/sales_order.json | 97 +++++++++++++++++++ .../selling_settings/selling_settings.json | 65 +++++++++++++ .../module_onboarding/selling/selling.json | 4 +- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_product/create_a_product.json | 4 +- .../create_a_quotation.json | 2 +- .../create_a_sales_order.json | 21 ++++ .../introduction_to_selling.json | 2 +- .../selling_settings/selling_settings.json | 8 +- .../setup_your_warehouse.json | 4 +- 10 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 erpnext/selling/form_tour/sales_order/sales_order.json create mode 100644 erpnext/selling/form_tour/selling_settings/selling_settings.json create mode 100644 erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json diff --git a/erpnext/selling/form_tour/sales_order/sales_order.json b/erpnext/selling/form_tour/sales_order/sales_order.json new file mode 100644 index 00000000000..a81eb4a043b --- /dev/null +++ b/erpnext/selling/form_tour/sales_order/sales_order.json @@ -0,0 +1,97 @@ +{ + "creation": "2021-06-29 21:13:36.089054", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 21:13:36.089054", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Order", + "owner": "Administrator", + "reference_doctype": "Sales Order", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer.", + "field": "", + "fieldname": "customer", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Customer", + "next_step_condition": "customer", + "parent_field": "", + "position": "Right", + "title": "Select Customer" + }, + { + "description": "You can add items here.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "position": "Bottom", + "title": "List of items" + }, + { + "child_doctype": "Sales Order Item", + "description": "Select an item.", + "field": "", + "fieldname": "item_code", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 1, + "label": "Item Code", + "next_step_condition": "eval: doc.item_code", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Select Item" + }, + { + "child_doctype": "Sales Order Item", + "description": "Enter quantity.", + "field": "", + "fieldname": "qty", + "fieldtype": "Float", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Quantity", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Quantity" + }, + { + "child_doctype": "Sales Order Item", + "description": "Enter rate of the item.", + "field": "", + "fieldname": "rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Rate", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Rate" + }, + { + "description": "You can add sales taxes and charges here.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Add Sales Taxes and Charges" + } + ], + "title": "Sales Order" +} \ No newline at end of file diff --git a/erpnext/selling/form_tour/selling_settings/selling_settings.json b/erpnext/selling/form_tour/selling_settings/selling_settings.json new file mode 100644 index 00000000000..20c718f8c0d --- /dev/null +++ b/erpnext/selling/form_tour/selling_settings/selling_settings.json @@ -0,0 +1,65 @@ +{ + "creation": "2021-06-29 20:39:19.408763", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 20:49:01.359489", + "modified_by": "Administrator", + "module": "Selling", + "name": "Selling Settings", + "owner": "Administrator", + "reference_doctype": "Selling Settings", + "save_on_complete": 0, + "steps": [ + { + "description": "By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a Naming Series. Choose the 'Naming Series' option.", + "field": "", + "fieldname": "cust_master_name", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Customer Naming By", + "parent_field": "", + "position": "Right", + "title": "Customer Naming By" + }, + { + "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.", + "field": "", + "fieldname": "so_required", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", + "parent_field": "", + "position": "Left", + "title": "Sales Order Required for Sales Invoice & Delivery Note Creation" + }, + { + "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.", + "field": "", + "fieldname": "dn_required", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Delivery Note Required for Sales Invoice Creation?", + "parent_field": "", + "position": "Left", + "title": "Delivery Note Required for Sales Invoice Creation" + }, + { + "description": "Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.", + "field": "", + "fieldname": "selling_price_list", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Price List", + "parent_field": "", + "position": "Right", + "title": "Default Selling Price List" + } + ], + "title": "Selling Settings" +} \ No newline at end of file diff --git a/erpnext/selling/module_onboarding/selling/selling.json b/erpnext/selling/module_onboarding/selling/selling.json index 160208ff680..29f51cbbc6d 100644 --- a/erpnext/selling/module_onboarding/selling/selling.json +++ b/erpnext/selling/module_onboarding/selling/selling.json @@ -19,7 +19,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/selling", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:05:37.669753", + "modified": "2021-06-29 21:23:24.510122", "modified_by": "Administrator", "module": "Selling", "name": "Selling", @@ -41,7 +41,7 @@ "step": "Create a Quotation" }, { - "step": "Create your first Sales Order" + "step": "Create a Sales Order" }, { "step": "Selling Settings" diff --git a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json index 5a403b06cf0..64defbfe3d3 100644 --- a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:16:19.731719", @@ -13,6 +12,7 @@ "name": "Create a Customer", "owner": "Administrator", "reference_document": "Customer", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Customer", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json index d2068e167b7..52a5861551c 100644 --- a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json +++ b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-12 18:30:02.489949", + "modified": "2020-10-14 14:53:00.133574", "modified_by": "Administrator", "name": "Create a Product", "owner": "Administrator", "reference_document": "Item", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Product", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json index 27253d15b6c..6f1837e24cd 100644 --- a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:34:58.958641", @@ -13,6 +12,7 @@ "name": "Create a Quotation", "owner": "Administrator", "reference_document": "Quotation", + "show_form_tour": 0, "show_full_form": 1, "title": "Create a Quotation", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json new file mode 100644 index 00000000000..378f295f0c0 --- /dev/null +++ b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Create Sales Order", + "creation": "2021-06-29 21:22:54.204880", + "description": "A Sales Order is a confirmation of an order from your customer. It is also referred to as Proforma Invoice.\n\nSales Order at the heart of your sales and purchase transactions. Sales Orders are linked in Delivery Note, Sales Invoices, Material Request, and Maintenance transactions. Through Sales Order, you can track fulfillment of the overall deal towards the customer.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-06-29 21:22:54.204880", + "modified_by": "Administrator", + "name": "Create a Sales Order", + "owner": "Administrator", + "reference_document": "Sales Order", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create a Sales Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json index d21c1f4954b..636453363bf 100644 --- a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json +++ b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json @@ -5,13 +5,13 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:29:13.703177", "modified_by": "Administrator", "name": "Introduction to Selling", "owner": "Administrator", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Selling", "validate_action": 1, diff --git a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json index 7996d7b1593..af95aa756f0 100644 --- a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json +++ b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json @@ -1,19 +1,21 @@ { "action": "Show Form Tour", + "action_label": "Let\u2019s walk-through Selling Settings", "creation": "2020-06-01 13:01:45.615189", + "description": "CRM and Selling module\u2019s features are configurable as per your business needs. Selling Settings is the place where you can set your preferences for:\n1. Customer naming and default values\n2. Billing and shipping preference in sales transactions\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2020-06-01 13:04:14.980743", + "modified": "2021-06-29 20:52:03.640254", "modified_by": "Administrator", "name": "Selling Settings", "owner": "Administrator", "reference_document": "Selling Settings", + "show_form_tour": 0, "show_full_form": 0, - "title": "Configure Selling Settings.", + "title": "Selling Settings", "validate_action": 0 } \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 9457deee262..1e20eb0eba7 100644 --- a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-07-04 12:33:16.970031", + "modified": "2020-10-14 14:53:25.538900", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", + "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 From f4d45d60f46d596931093482e65b537c90a58559 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Fri, 9 Jul 2021 20:45:43 +0530 Subject: [PATCH 358/376] Revert "fix: updated onboarding steps for selling module" (#26414) This reverts commit 446171acc4232ec964473804e73ab6dfc413081d. --- .../form_tour/sales_order/sales_order.json | 97 ------------------- .../selling_settings/selling_settings.json | 65 ------------- .../module_onboarding/selling/selling.json | 4 +- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_product/create_a_product.json | 4 +- .../create_a_quotation.json | 2 +- .../create_a_sales_order.json | 21 ---- .../introduction_to_selling.json | 2 +- .../selling_settings/selling_settings.json | 8 +- .../setup_your_warehouse.json | 4 +- 10 files changed, 12 insertions(+), 197 deletions(-) delete mode 100644 erpnext/selling/form_tour/sales_order/sales_order.json delete mode 100644 erpnext/selling/form_tour/selling_settings/selling_settings.json delete mode 100644 erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json diff --git a/erpnext/selling/form_tour/sales_order/sales_order.json b/erpnext/selling/form_tour/sales_order/sales_order.json deleted file mode 100644 index a81eb4a043b..00000000000 --- a/erpnext/selling/form_tour/sales_order/sales_order.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "creation": "2021-06-29 21:13:36.089054", - "docstatus": 0, - "doctype": "Form Tour", - "idx": 0, - "is_standard": 1, - "modified": "2021-06-29 21:13:36.089054", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Order", - "owner": "Administrator", - "reference_doctype": "Sales Order", - "save_on_complete": 1, - "steps": [ - { - "description": "Select a customer.", - "field": "", - "fieldname": "customer", - "fieldtype": "Link", - "has_next_condition": 1, - "is_table_field": 0, - "label": "Customer", - "next_step_condition": "customer", - "parent_field": "", - "position": "Right", - "title": "Select Customer" - }, - { - "description": "You can add items here.", - "field": "", - "fieldname": "items", - "fieldtype": "Table", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Items", - "parent_field": "", - "position": "Bottom", - "title": "List of items" - }, - { - "child_doctype": "Sales Order Item", - "description": "Select an item.", - "field": "", - "fieldname": "item_code", - "fieldtype": "Link", - "has_next_condition": 1, - "is_table_field": 1, - "label": "Item Code", - "next_step_condition": "eval: doc.item_code", - "parent_field": "", - "parent_fieldname": "items", - "position": "Right", - "title": "Select Item" - }, - { - "child_doctype": "Sales Order Item", - "description": "Enter quantity.", - "field": "", - "fieldname": "qty", - "fieldtype": "Float", - "has_next_condition": 0, - "is_table_field": 1, - "label": "Quantity", - "parent_field": "", - "parent_fieldname": "items", - "position": "Right", - "title": "Enter Quantity" - }, - { - "child_doctype": "Sales Order Item", - "description": "Enter rate of the item.", - "field": "", - "fieldname": "rate", - "fieldtype": "Currency", - "has_next_condition": 0, - "is_table_field": 1, - "label": "Rate", - "parent_field": "", - "parent_fieldname": "items", - "position": "Right", - "title": "Enter Rate" - }, - { - "description": "You can add sales taxes and charges here.", - "field": "", - "fieldname": "taxes", - "fieldtype": "Table", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Sales Taxes and Charges", - "parent_field": "", - "position": "Bottom", - "title": "Add Sales Taxes and Charges" - } - ], - "title": "Sales Order" -} \ No newline at end of file diff --git a/erpnext/selling/form_tour/selling_settings/selling_settings.json b/erpnext/selling/form_tour/selling_settings/selling_settings.json deleted file mode 100644 index 20c718f8c0d..00000000000 --- a/erpnext/selling/form_tour/selling_settings/selling_settings.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "creation": "2021-06-29 20:39:19.408763", - "docstatus": 0, - "doctype": "Form Tour", - "idx": 0, - "is_standard": 1, - "modified": "2021-06-29 20:49:01.359489", - "modified_by": "Administrator", - "module": "Selling", - "name": "Selling Settings", - "owner": "Administrator", - "reference_doctype": "Selling Settings", - "save_on_complete": 0, - "steps": [ - { - "description": "By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a Naming Series. Choose the 'Naming Series' option.", - "field": "", - "fieldname": "cust_master_name", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Customer Naming By", - "parent_field": "", - "position": "Right", - "title": "Customer Naming By" - }, - { - "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.", - "field": "", - "fieldname": "so_required", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", - "parent_field": "", - "position": "Left", - "title": "Sales Order Required for Sales Invoice & Delivery Note Creation" - }, - { - "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.", - "field": "", - "fieldname": "dn_required", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Is Delivery Note Required for Sales Invoice Creation?", - "parent_field": "", - "position": "Left", - "title": "Delivery Note Required for Sales Invoice Creation" - }, - { - "description": "Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.", - "field": "", - "fieldname": "selling_price_list", - "fieldtype": "Link", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Default Price List", - "parent_field": "", - "position": "Right", - "title": "Default Selling Price List" - } - ], - "title": "Selling Settings" -} \ No newline at end of file diff --git a/erpnext/selling/module_onboarding/selling/selling.json b/erpnext/selling/module_onboarding/selling/selling.json index 29f51cbbc6d..160208ff680 100644 --- a/erpnext/selling/module_onboarding/selling/selling.json +++ b/erpnext/selling/module_onboarding/selling/selling.json @@ -19,7 +19,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/selling", "idx": 0, "is_complete": 0, - "modified": "2021-06-29 21:23:24.510122", + "modified": "2020-07-08 14:05:37.669753", "modified_by": "Administrator", "module": "Selling", "name": "Selling", @@ -41,7 +41,7 @@ "step": "Create a Quotation" }, { - "step": "Create a Sales Order" + "step": "Create your first Sales Order" }, { "step": "Selling Settings" diff --git a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json index 64defbfe3d3..5a403b06cf0 100644 --- a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json @@ -5,6 +5,7 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:16:19.731719", @@ -12,7 +13,6 @@ "name": "Create a Customer", "owner": "Administrator", "reference_document": "Customer", - "show_form_tour": 0, "show_full_form": 0, "title": "Create a Customer", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json index 52a5861551c..d2068e167b7 100644 --- a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json +++ b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.133574", + "modified": "2020-05-12 18:30:02.489949", "modified_by": "Administrator", "name": "Create a Product", "owner": "Administrator", "reference_document": "Item", - "show_form_tour": 0, "show_full_form": 0, "title": "Create a Product", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json index 6f1837e24cd..27253d15b6c 100644 --- a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json @@ -5,6 +5,7 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:34:58.958641", @@ -12,7 +13,6 @@ "name": "Create a Quotation", "owner": "Administrator", "reference_document": "Quotation", - "show_form_tour": 0, "show_full_form": 1, "title": "Create a Quotation", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json deleted file mode 100644 index 378f295f0c0..00000000000 --- a/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "action": "Create Entry", - "action_label": "Create Sales Order", - "creation": "2021-06-29 21:22:54.204880", - "description": "A Sales Order is a confirmation of an order from your customer. It is also referred to as Proforma Invoice.\n\nSales Order at the heart of your sales and purchase transactions. Sales Orders are linked in Delivery Note, Sales Invoices, Material Request, and Maintenance transactions. Through Sales Order, you can track fulfillment of the overall deal towards the customer.", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2021-06-29 21:22:54.204880", - "modified_by": "Administrator", - "name": "Create a Sales Order", - "owner": "Administrator", - "reference_document": "Sales Order", - "show_form_tour": 1, - "show_full_form": 1, - "title": "Create a Sales Order", - "validate_action": 1 -} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json index 636453363bf..d21c1f4954b 100644 --- a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json +++ b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json @@ -5,13 +5,13 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:29:13.703177", "modified_by": "Administrator", "name": "Introduction to Selling", "owner": "Administrator", - "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Selling", "validate_action": 1, diff --git a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json index af95aa756f0..7996d7b1593 100644 --- a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json +++ b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json @@ -1,21 +1,19 @@ { "action": "Show Form Tour", - "action_label": "Let\u2019s walk-through Selling Settings", "creation": "2020-06-01 13:01:45.615189", - "description": "CRM and Selling module\u2019s features are configurable as per your business needs. Selling Settings is the place where you can set your preferences for:\n1. Customer naming and default values\n2. Billing and shipping preference in sales transactions\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2021-06-29 20:52:03.640254", + "modified": "2020-06-01 13:04:14.980743", "modified_by": "Administrator", "name": "Selling Settings", "owner": "Administrator", "reference_document": "Selling Settings", - "show_form_tour": 0, "show_full_form": 0, - "title": "Selling Settings", + "title": "Configure Selling Settings.", "validate_action": 0 } \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 1e20eb0eba7..9457deee262 100644 --- a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:25.538900", + "modified": "2020-07-04 12:33:16.970031", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", - "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 From 862ce916ae1e9b3ab60f4a2316c47a7f1aa6aa3c Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> Date: Fri, 9 Jul 2021 21:52:50 +0530 Subject: [PATCH 359/376] fix: Nested/Multi-level BOM help link (#26409) Updated the link for multi-level boms. Current link is broken. --- erpnext/public/js/help_links.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index aa9bba17c77..5c9a453e7df 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [ label: "Nested BOM Structure", url: docsUrl + - "user/manual/en/manufacturing/articles/nested-bom-structure", + "user/manual/en/manufacturing/articles/managing-multi-level-bom", }, ]; From 727e5d16bcf381fc2a35d41cb0e21e45a3856c24 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 21:55:37 +0530 Subject: [PATCH 360/376] chore: add .backportrc to gitignore (#26403) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 63c51c49765..89f56263ffa 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ __pycache__ .idea/ .vscode/ node_modules/ +.backportrc.json \ No newline at end of file From 9ac9a4ef2120ceccf1e3893cc20b5c0261ae7928 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 16:18:35 +0530 Subject: [PATCH 361/376] feat: Optionally allow rejected quality inspection on submission --- erpnext/controllers/stock_controller.py | 84 ++++++++++++------- .../stock_entry_detail.json | 3 +- .../stock_settings/stock_settings.json | 21 ++++- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8196cff849d..fed7c0a581a 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -356,42 +356,68 @@ class StockController(AccountsController): }, update_modified) def validate_inspection(self): - '''Checks if quality inspection is set for Items that require inspection. - On submit, throw an exception''' - inspection_required_fieldname = None - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - inspection_required_fieldname = "inspection_required_before_purchase" - elif self.doctype in ["Delivery Note", "Sales Invoice"]: - inspection_required_fieldname = "inspection_required_before_delivery" + """Checks if quality inspection is set/ is valid for Items that require inspection.""" + inspection_fieldname_map = { + "Purchase Receipt": "inspection_required_before_purchase", + "Purchase Invoice": "inspection_required_before_purchase", + "Sales Invoice": "inspection_required_before_delivery", + "Delivery Note": "inspection_required_before_delivery" + } + inspection_required_fieldname = inspection_fieldname_map.get(self.doctype) + # return if inspection is not required on document level if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or (self.doctype == "Stock Entry" and not self.inspection_required) or (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): return - for d in self.get('items'): - qa_required = False - if (inspection_required_fieldname and not d.quality_inspection and - frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): - qa_required = True - elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: - qa_required = True - if self.docstatus == 1 and d.quality_inspection: - qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) - if qa_doc.docstatus == 0: - link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) - frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) + for row in self.get('items'): + qi_required = False + if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)): + qi_required = True + elif self.doctype == "Stock Entry" and row.t_warehouse: + qi_required = True # inward stock needs inspection - if qa_doc.status != 'Accepted': - frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") - .format(d.idx, d.item_code), QualityInspectionRejectedError) - elif qa_required : - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted - if self.docstatus==1 and action == 'Stop': - frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), - exc=QualityInspectionRequiredError) - else: - frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) + if qi_required: # validate row only if inspection is required on item level + self.validate_qi_presence(row) + if self.docstatus == 1: + self.validate_qi_submission(row) + self.validate_qi_rejection(row) + + def validate_qi_presence(self, row): + """Check if QI is present on row level. Warn on save and stop on submit if missing.""" + if not row.quality_inspection: + msg = _(f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}") + if self.docstatus == 1: + frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError) + else: + frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue") + + def validate_qi_submission(self, row): + """Check if QI is submitted on row level, during submission""" + action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted or "Stop" + qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") + + if not qa_docstatus == 1: + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = _(f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}") + if action == "Stop": + frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + else: + frappe.msgprint(msg, alert=True) + + def validate_qi_rejection(self, row): + """Check if QI is rejected on row level, during submission""" + action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_rejected or "Stop" + qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") + + if qa_status == "Rejected": + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = _(f"Row #{row.idx}: Quality Inspection was rejected for item {row.item_code}") + if action == "Stop": + frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + else: + frappe.msgprint(msg, alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index a1782839048..22f412a2989 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -307,6 +307,7 @@ "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", + "no_copy": 1, "options": "Quality Inspection" }, { @@ -548,7 +549,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-22 20:08:23.799715", + "modified": "2021-06-21 16:03:18.834880", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index cf5d98d0923..d07e26b5366 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -23,7 +23,10 @@ "allow_negative_stock", "show_barcode_field", "clean_description_html", + "quality_inspection_settings_section", "action_if_quality_inspection_is_not_submitted", + "column_break_21", + "action_if_quality_inspection_is_rejected", "section_break_7", "automatically_set_serial_nos_based_on_fifo", "set_qty_in_transactions_based_on_serial_no_input", @@ -264,6 +267,22 @@ { "fieldname": "column_break_31", "fieldtype": "Column Break" + }, + { + "fieldname": "quality_inspection_settings_section", + "fieldtype": "Section Break", + "label": "Quality Inspection Settings" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "default": "Stop", + "fieldname": "action_if_quality_inspection_is_rejected", + "fieldtype": "Select", + "label": "Action If Quality Inspection Is Rejected", + "options": "Stop\nWarn" } ], "icon": "icon-cog", @@ -271,7 +290,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-30 17:27:42.709231", + "modified": "2021-06-21 16:17:42.159829", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From 654e9d85d182bd486b74cd72eebf6f254391a35f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 16:51:12 +0530 Subject: [PATCH 362/376] fix: sider and semgrep --- erpnext/controllers/stock_controller.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fed7c0a581a..59d34d1ce52 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -387,11 +387,11 @@ class StockController(AccountsController): def validate_qi_presence(self, row): """Check if QI is present on row level. Warn on save and stop on submit if missing.""" if not row.quality_inspection: - msg = _(f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}") + msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" if self.docstatus == 1: - frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError) + frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) else: - frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue") + frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" @@ -400,11 +400,11 @@ class StockController(AccountsController): if not qa_docstatus == 1: link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) - msg = _(f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}") + msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" if action == "Stop": - frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: - frappe.msgprint(msg, alert=True) + frappe.msgprint(_(msg), alert=True) def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" @@ -413,11 +413,11 @@ class StockController(AccountsController): if qa_status == "Rejected": link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) - msg = _(f"Row #{row.idx}: Quality Inspection was rejected for item {row.item_code}") + msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" if action == "Stop": - frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) else: - frappe.msgprint(msg, alert=True, indicator="orange") + frappe.msgprint(_(msg), alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) From 9ba3fce4fd1cb300576d6ddffb79bd008a2ee003 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 22 Jun 2021 11:20:17 +0530 Subject: [PATCH 363/376] fix: Consistent alert indicators --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 59d34d1ce52..1749297ce39 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -404,7 +404,7 @@ class StockController(AccountsController): if action == "Stop": frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: - frappe.msgprint(_(msg), alert=True) + frappe.msgprint(_(msg), alert=True, indicator="orange") def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" From f67f13c3844ccb96308f2ef9825a7d55713a2ae4 Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 10 Jul 2021 18:24:24 +0530 Subject: [PATCH 364/376] chore: Test case for QI Rejection in Stock Entry - Use `get_single_value` instead of `get_doc` in validation - Test Case to check impact of stock settings on SE with rejected qi --- erpnext/controllers/stock_controller.py | 4 +- .../test_quality_inspection.py | 48 +++++++++++++++++-- .../doctype/stock_entry/stock_entry_utils.py | 2 + .../stock_settings/stock_settings.json | 2 +- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 1749297ce39..2526e6df0ef 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -395,7 +395,7 @@ class StockController(AccountsController): def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted or "Stop" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") if not qa_docstatus == 1: @@ -408,7 +408,7 @@ class StockController(AccountsController): def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_rejected or "Stop" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected") qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") if qa_status == "Rejected": diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 7f3d701034a..f5d076a077a 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -14,7 +14,7 @@ from erpnext.controllers.stock_controller import ( ) from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # test_records = frappe.get_test_records('Quality Inspection') @@ -159,6 +159,47 @@ class TestQualityInspection(unittest.TestCase): frappe.delete_doc("Quality Inspection", qi) dn.delete() + def test_rejected_qi_validation(self): + """Test if rejected QI blocks Stock Entry as per Stock Settings.""" + se = make_stock_entry( + item_code="_Test Item with QA", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=100, + inspection_required=True, + do_not_submit=True + ) + + readings = [ + { + "specification": "Iron Content", + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + } + ] + + qa = create_quality_inspection( + reference_type="Stock Entry", + reference_name=se.name, + readings=readings, + status="Rejected" + ) + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + se.reload() + self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") + se.reload() + se.submit() # when allowed in Stock settings, allow rejected QI + + # teardown + qa.reload() + qa.cancel() + se.reload() + se.cancel() + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") def create_quality_inspection(**args): args = frappe._dict(args) @@ -175,12 +216,11 @@ def create_quality_inspection(**args): if not args.readings: create_quality_inspection_parameter("Size") readings = {"specification": "Size", "min_value": 0, "max_value": 10} + if args.status == "Rejected": + readings["reading_1"] = "12" # status is auto set in child on save else: readings = args.readings - if args.status == "Rejected": - readings["reading_1"] = "12" # status is auto set in child on save - if isinstance(readings, list): for entry in readings: create_quality_inspection_parameter(entry["specification"]) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b12a8547fea..563fcb03973 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -45,6 +45,8 @@ def make_stock_entry(**args): s.posting_date = args.posting_date if args.posting_time: s.posting_time = args.posting_time + if args.inspection_required: + s.inspection_required = args.inspection_required # map names if args.from_warehouse: diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index d07e26b5366..2a9dcfb67ed 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -290,7 +290,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-21 16:17:42.159829", + "modified": "2021-07-10 16:17:42.159829", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From b99469cdabc646cdfa55ae1ce1709374c7246a7a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 20:18:53 +0530 Subject: [PATCH 365/376] fix: stock levels disapperaing on refresh refresh_section removes all sections with `custom` class, added different class to avoid this behaviour. --- erpnext/stock/doctype/item/item.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 45e3c21b27d..568f0ef451e 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -93,7 +93,7 @@ frappe.ui.form.on("Item", { erpnext.item.edit_prices_button(frm); erpnext.item.toggle_attributes(frm); - + if (!frm.doc.is_fixed_asset) { erpnext.item.make_dashboard(frm); } @@ -381,7 +381,8 @@ $.extend(erpnext.item, { // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { frappe.require('item-dashboard.bundle.js', function() { - const section = frm.dashboard.add_section('', __("Stock Levels")); + frm.dashboard.parent.find('.stock-levels').remove(); + const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels'); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, From bedb0addf0ca7e81ca10379f1830d42167811ca4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 20:36:51 +0530 Subject: [PATCH 366/376] test: ui test for stock levels --- cypress/integration/test_item.js | 44 ++++++++++++++++++++++++++++++++ cypress/support/commands.js | 6 +++++ 2 files changed, 50 insertions(+) create mode 100644 cypress/integration/test_item.js diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js new file mode 100644 index 00000000000..fcb7533a225 --- /dev/null +++ b/cypress/integration/test_item.js @@ -0,0 +1,44 @@ +describe("Test Item Dashboard", () => { + before(() => { + cy.login(); + cy.visit("/app/item"); + cy.insert_doc( + "Item", + { + item_code: "e2e_test_item", + item_group: "All Item Groups", + opening_stock: 42, + valuation_rate: 100, + }, + true + ); + cy.go_to_doc("item", "e2e_test_item"); + }); + + it("should show dashboard with correct data on first load", () => { + cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); + cy.get(".stock-levels").contains("e2e_test_item").should("exist"); + + // reserved and available qty + cy.get(".stock-levels .inline-graph-count") + .eq(0) + .contains("0") + .should("exist"); + cy.get(".stock-levels .inline-graph-count") + .eq(1) + .contains("42") + .should("exist"); + }); + + it("should persist on field change", () => { + cy.get('input[data-fieldname="disabled"]').check(); + cy.wait(500); + cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); + cy.get(".stock-levels").should("have.length", 1); + }); + + it("should persist on reload", () => { + cy.reload(); + cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7929a2e0ef5..7ddc80ab8dd 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -23,3 +23,9 @@ // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); + +const slug = (name) => name.toLowerCase().replace(" ", "-"); + +Cypress.Commands.add("go_to_doc", (doctype, name) => { + cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`); +}); From 203c2b22924d9a8c25ba2bec7c3417e6733b3992 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 22:35:15 +0530 Subject: [PATCH 367/376] ci: show bench logs if ui test fails --- .github/helper/install.sh | 2 +- .github/workflows/ui-tests.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index f7a71223436..455ab861f93 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -42,6 +42,6 @@ sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile bench get-app erpnext "${GITHUB_WORKSPACE}" -bench start & +bench start &> bench_run_logs.txt & bench --site test_site reinstall --yes bench build --app frappe diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 4bc55da1d8f..412a05b0a15 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -102,3 +102,7 @@ jobs: run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + - name: Show bench console if tests failed + if: ${{ failure() }} + run: cat ~/frappe-bench/bench_run_logs.txt From 22cb6428316b8498103e68694902aed051a88359 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 23:32:33 +0530 Subject: [PATCH 368/376] chore: update test company FY --- erpnext/setup/utils.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 13269a82823..d5dbd4cc659 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -28,21 +28,21 @@ def before_tests(): from frappe.desk.page.setup_wizard.setup_wizard import setup_complete if not frappe.get_list("Company"): setup_complete({ - "currency" :"USD", - "full_name" :"Test User", - "company_name" :"Wind Power LLC", - "timezone" :"America/New_York", - "company_abbr" :"WP", - "industry" :"Manufacturing", - "country" :"United States", - "fy_start_date" :"2011-01-01", - "fy_end_date" :"2011-12-31", - "language" :"english", - "company_tagline" :"Testing", - "email" :"test@erpnext.com", - "password" :"test", + "currency" :"USD", + "full_name" :"Test User", + "company_name" :"Wind Power LLC", + "timezone" :"America/New_York", + "company_abbr" :"WP", + "industry" :"Manufacturing", + "country" :"United States", + "fy_start_date" :"2021-01-01", + "fy_end_date" :"2021-12-31", + "language" :"english", + "company_tagline" :"Testing", + "email" :"test@erpnext.com", + "password" :"test", "chart_of_accounts" : "Standard", - "domains" : ["Manufacturing"], + "domains" : ["Manufacturing"], }) frappe.db.sql("delete from `tabLeave Allocation`") From 6467632f6088f64baadf9235ebeede7385bcc0a9 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:07:06 +0530 Subject: [PATCH 369/376] fix: error popup for COA errors (#26357) --- .../chart_of_accounts_importer.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 3b764aab103..cb1f2df7f01 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -13,7 +13,9 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file class ChartofAccountsImporter(Document): - pass + def validate(self): + validate_accounts(self.import_file) + @frappe.whitelist() def validate_company(company): @@ -301,28 +303,28 @@ def validate_accounts(file_name): if account["parent_account"] and accounts_dict.get(account["parent_account"]): accounts_dict[account["parent_account"]]["is_group"] = 1 - message = validate_root(accounts_dict) - if message: return message - message = validate_account_types(accounts_dict) - if message: return message + validate_root(accounts_dict) + + validate_account_types(accounts_dict) + return [True, len(accounts)] def validate_root(accounts): roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] if len(roots) < 4: - return _("Number of root accounts cannot be less than 4") + frappe.throw(_("Number of root accounts cannot be less than 4")) error_messages = [] for account in roots: if not account.get("root_type") and account.get("account_name"): - error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name"))) + error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name"))) elif account.get("root_type") not in get_root_types() and account.get("account_name"): - error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name"))) + error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name"))) if error_messages: - return "
    ".join(error_messages) + frappe.throw("
    ".join(error_messages)) def get_root_types(): return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') @@ -356,7 +358,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_ledger) - set(account_types)) if missing: - return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))) account_types_for_group = ["Bank", "Cash", "Stock"] # fix logic bug @@ -364,7 +366,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_group) - set(account_groups)) if missing: - return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))) def unset_existing_data(company): linked = frappe.db.sql('''select fieldname from tabDocField From 4b4ac7c05198f778be01c197e04482ab8e4cf142 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:10:00 +0530 Subject: [PATCH 370/376] fix(report): iterate on accounts only when accounts exist (#26390) --- erpnext/accounts/report/general_ledger/general_ledger.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 744ada9e558..e724e9b51b6 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -48,13 +48,12 @@ def validate_filters(filters, account_details): if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - - for account in filters.account: - if not account_details.get(account): - frappe.throw(_("Account {0} does not exists").format(account)) if filters.get('account'): filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if not account_details.get(account): + frappe.throw(_("Account {0} does not exists").format(account)) if (filters.get("account") and filters.get("group_by") == _('Group by Account') and account_details[filters.account].is_group == 0): From 7e05bea6a9fd3d2d6402c5e22d11b68e1225a774 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:11:58 +0530 Subject: [PATCH 371/376] fix: dunning calculation of grand total when rate of interest is 0% (#26286) --- erpnext/accounts/doctype/dunning/dunning.py | 8 ++-- .../accounts/doctype/dunning/test_dunning.py | 48 ++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c6c689212b6..1ef512a4894 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -25,7 +25,7 @@ class Dunning(AccountsController): def validate_amount(self): amounts = calculate_interest_and_amount( - self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) + self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) if self.interest_amount != amounts.get('interest_amount'): self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) if self.dunning_amount != amounts.get('dunning_amount'): @@ -91,13 +91,13 @@ def resolve_dunning(doc, state): for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') -def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): +def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): interest_amount = 0 - grand_total = 0 + grand_total = flt(outstanding_amount) + flt(dunning_fee) if rate_of_interest: interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) + grand_total += flt(interest_amount) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { 'interest_amount': interest_amount, diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e2d4d82e418..31cb078cd4f 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase): @classmethod def setUpClass(self): create_dunning_type() + create_dunning_type_with_zero_interest_rate() unlink_payment_on_cancel_of_invoice() @classmethod @@ -25,11 +26,19 @@ class TestDunning(unittest.TestCase): def test_dunning(self): dunning = create_dunning() amounts = calculate_interest_and_amount( - dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) + def test_dunning_with_zero_interest_rate(self): + dunning = create_dunning_with_zero_interest_rate() + amounts = calculate_interest_and_amount( + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + self.assertEqual(round(amounts.get('interest_amount'), 2), 0) + self.assertEqual(round(amounts.get('dunning_amount'), 2), 20) + self.assertEqual(round(amounts.get('grand_total'), 2), 120) + def test_gl_entries(self): dunning = create_dunning() dunning.submit() @@ -83,6 +92,27 @@ def create_dunning(): dunning.save() return dunning +def create_dunning_with_zero_interest_rate(): + posting_date = add_days(today(), -20) + due_date = add_days(today(), -15) + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=posting_date, due_date=due_date, status='Overdue') + dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest') + dunning = frappe.new_doc("Dunning") + dunning.sales_invoice = sales_invoice.name + dunning.customer_name = sales_invoice.customer_name + dunning.outstanding_amount = sales_invoice.outstanding_amount + dunning.debit_to = sales_invoice.debit_to + dunning.currency = sales_invoice.currency + dunning.company = sales_invoice.company + dunning.posting_date = nowdate() + dunning.due_date = sales_invoice.due_date + dunning.dunning_type = 'First Notice with 0% Rate of Interest' + dunning.rate_of_interest = dunning_type.rate_of_interest + dunning.dunning_fee = dunning_type.dunning_fee + dunning.save() + return dunning + def create_dunning_type(): dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = 'First Notice' @@ -98,3 +128,19 @@ def create_dunning_type(): } ) dunning_type.save() + +def create_dunning_type_with_zero_interest_rate(): + dunning_type = frappe.new_doc("Dunning Type") + dunning_type.dunning_type = 'First Notice with 0% Rate of Interest' + dunning_type.start_day = 10 + dunning_type.end_day = 20 + dunning_type.dunning_fee = 20 + dunning_type.rate_of_interest = 0 + dunning_type.append( + "dunning_letter_text", { + 'language': 'en', + 'body_text': 'We have still not received payment for our invoice ', + 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.' + } + ) + dunning_type.save() \ No newline at end of file From b4ea185ecaabe3c8080aa1bb0fdcb643d850f5d7 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Mon, 12 Jul 2021 13:15:41 +0530 Subject: [PATCH 372/376] fix: added company filter while fetching loans (#26295) * fix: added company filter while fetching loans * fix: added set_query in refresh * fix: quotes * fix: tests Co-authored-by: Rucha Mahabal --- erpnext/loan_management/doctype/loan/loan.js | 3 +- .../loan_application/loan_application.js | 13 +++++-- .../doctype/salary_slip/salary_slip.py | 1 + .../doctype/salary_slip/test_salary_slip.py | 14 ++++--- .../salary_structure/test_salary_structure.py | 37 +++++++++---------- 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 28af3a9c415..f9c201ab603 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -28,7 +28,8 @@ frappe.ui.form.on('Loan', { frm.set_query("loan_type", function () { return { "filters": { - "docstatus": 1 + "docstatus": 1, + "company": frm.doc.company } }; }); diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index 13652749711..017026ca13f 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -14,11 +14,18 @@ frappe.ui.form.on('Loan Application', { refresh: function(frm) { frm.trigger("toggle_fields"); frm.trigger("add_toolbar_buttons"); + frm.set_query("loan_type", () => { + return { + filters: { + company: frm.doc.company + } + }; + }); }, repayment_method: function(frm) { - frm.doc.repayment_amount = frm.doc.repayment_periods = "" - frm.trigger("toggle_fields") - frm.trigger("toggle_required") + frm.doc.repayment_amount = frm.doc.repayment_periods = ""; + frm.trigger("toggle_fields"); + frm.trigger("toggle_required"); }, toggle_fields: function(frm) { frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period") diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index c55bec89be8..f82b0d51bb1 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1088,6 +1088,7 @@ class SalarySlip(TransactionBase): "applicant": self.employee, "docstatus": 1, "repay_from_salary": 1, + "company": self.company }) def make_loan_repayment_entry(self): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index ce88cc3f1e1..d730fcf1fa9 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -481,15 +481,19 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", + { + "user_id": user + }, + ["name", "company", "employee_name"], + as_dict=True) - employee = frappe.db.get_value("Employee", {"user_id": user}) - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) if not salary_slip_name: - salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee) - salary_slip.employee_name = frappe.get_value("Employee", - {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name") + salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name) + salary_slip.employee_name = employee.employee_name salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e7d123c9960..374dd7ee443 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -119,26 +119,25 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) - if not frappe.db.exists('Salary Structure', salary_structure): - details = { - "doctype": "Salary Structure", - "name": salary_structure, - "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account", filters={'account_currency': currency}), - "currency": currency - } - if other_details and isinstance(other_details, dict): - details.update(other_details) - salary_structure_doc = frappe.get_doc(details) - salary_structure_doc.insert() - if not dont_submit: - salary_structure_doc.submit() + if frappe.db.exists("Salary Structure", salary_structure): + frappe.db.delete("Salary Structure", salary_structure) - else: - salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) + details = { + "doctype": "Salary Structure", + "name": salary_structure, + "company": company or erpnext.get_default_company(), + "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "payroll_frequency": payroll_frequency, + "payment_account": get_random("Account", filters={"account_currency": currency}), + "currency": currency + } + if other_details and isinstance(other_details, dict): + details.update(other_details) + salary_structure_doc = frappe.get_doc(details) + salary_structure_doc.insert() + if not dont_submit: + salary_structure_doc.submit() filters = {'employee':employee, 'docstatus': 1} if not from_date and payroll_period: From 97bce3af9a3f5d2fd56c4e7f121708b486520cf6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Jul 2021 13:24:43 +0530 Subject: [PATCH 373/376] refactor: Optimized code for reposting item valuation --- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../stock_ledger_entry/stock_ledger_entry.py | 1 + erpnext/stock/stock_ledger.py | 61 +++++++++++++++---- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8f27ef4356c..90b81ddb1dc 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -529,7 +529,7 @@ class StockEntry(StockController): 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 frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if 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()]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 0febcb68910..cb939e63c28 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -178,3 +178,4 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) + frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"]) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4e9c7689ae4..c15d1eda7dc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -6,13 +6,14 @@ import frappe import erpnext import copy from frappe import _ -from frappe.utils import cint, flt, cstr, now, get_link_to_form +from frappe.utils import cint, flt, cstr, now, get_link_to_form, getdate from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel from erpnext.stock.utils import get_bin import json from six import iteritems + # future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): @@ -130,7 +131,13 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat if not args and voucher_type and voucher_no: args = get_args_for_voucher(voucher_type, voucher_no) - distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] + distinct_item_warehouses = {} + for i, d in enumerate(args): + distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ + "reposting_status": False, + "sle": d, + "args_idx": i + })) i = 0 while i < len(args): @@ -139,13 +146,21 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat "warehouse": args[i].warehouse, "posting_date": args[i].posting_date, "posting_time": args[i].posting_time, - "creation": args[i].get("creation") + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - for item_wh, new_sle in iteritems(obj.new_items): - if item_wh not in distinct_item_warehouses: - args.append(new_sle) + distinct_item_warehouses[(args[i].item_code, args[i].warehouse)].reposting_status = True + if obj.new_items_found: + for item_wh, data in iteritems(distinct_item_warehouses): + if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + data.args_idx = len(args) + args.append(data.sle) + elif data.sle_changed and not data.reposting_status: + args[data.args_idx] = data.sle + + data.sle_changed = False i += 1 def get_args_for_voucher(voucher_type, voucher_no): @@ -186,11 +201,12 @@ class update_entries_after(object): self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() self.valuation_method = get_valuation_method(self.item_code) - self.new_items = {} + + self.new_items_found = False + self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.data = frappe._dict() self.initialize_previous_data(self.args) - self.build() def get_precision(self): @@ -296,11 +312,29 @@ class update_entries_after(object): elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: return entries_to_fix elif dependant_sle.item_code != self.item_code: - if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: - self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle + self.update_distinct_item_warehouses(dependant_sle) return entries_to_fix elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix + else: + return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + + def update_distinct_item_warehouses(self, dependant_sle): + key = (dependant_sle.item_code, dependant_sle.warehouse) + val = frappe._dict({ + "sle": dependant_sle + }) + if key not in self.distinct_item_warehouses: + self.distinct_item_warehouses[key] = val + self.new_items_found = True + else: + existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): + val.sle_changed = True + self.distinct_item_warehouses[key] = val + self.new_items_found = True + + def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) args = self.data[dependant_sle.warehouse].previous_sle \ @@ -393,6 +427,7 @@ class update_entries_after(object): rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": + self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): @@ -442,7 +477,11 @@ class update_entries_after(object): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True) + if not sle.dependant_sle_voucher_detail_no: + self.recalculate_amounts_in_stock_entry(sle.voucher_no) + + def recalculate_amounts_in_stock_entry(self, voucher_no): + stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: From 9e1819d36675bec1096fb34f603889dc647ae290 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Jul 2021 14:31:57 +0530 Subject: [PATCH 374/376] fix: move the rename abbreviation job to long queue (#26434) --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 0427abe558d..8fd905d7a79 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -395,7 +395,7 @@ class Company(NestedSet): @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue="long", company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) From a20999cfbcbb950bb1dc34838dc6bb1dd04d24ae Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Jul 2021 14:33:23 +0530 Subject: [PATCH 375/376] fix: exchange gain loss not set for advances linked with invoices (#25369) --- .../doctype/payment_entry/payment_entry.py | 18 +- .../payment_entry_reference.json | 12 +- .../purchase_invoice/purchase_invoice.py | 1 + .../purchase_invoice/test_purchase_invoice.py | 103 ++++++ .../purchase_invoice_advance.json | 330 ++++++----------- .../doctype/sales_invoice/sales_invoice.py | 1 + .../sales_invoice_advance.json | 331 ++++++------------ erpnext/accounts/utils.py | 14 +- erpnext/controllers/accounts_controller.py | 86 ++++- 9 files changed, 441 insertions(+), 455 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0c21aae944c..ff00fde523f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -183,6 +183,13 @@ class PaymentEntry(AccountsController): d.reference_name, self.party_account_currency) for field, value in iteritems(ref_details): + if d.exchange_gain_loss: + # for cases where gain/loss is booked into invoice + # exchange_gain_loss is calculated from invoice & populated + # and row.exchange_rate is already set to payment entry's exchange rate + # refer -> `update_reference_in_payment_entry()` in utils.py + continue + if field == 'exchange_rate' or not d.get(field) or force: d.db_set(field, value) @@ -664,8 +671,8 @@ class PaymentEntry(AccountsController): gl_entries.append(gle) if self.unallocated_amount: - base_unallocated_amount = self.unallocated_amount * \ - (self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate) + exchange_rate = self.get_exchange_rate() + base_unallocated_amount = (self.unallocated_amount * exchange_rate) gle = party_gl_dict.copy() @@ -806,10 +813,17 @@ class PaymentEntry(AccountsController): if account_details: row.update(account_details) + + if not row.get('amount'): + # if no difference amount + return self.append('deductions', row) self.set_unallocated_amount() + def get_exchange_rate(self): + return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate + def initialize_taxes(self): for tax in self.get("taxes"): validate_taxes_and_charges(tax) diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 912ad0977a2..43eb0b6e2aa 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -14,7 +14,8 @@ "total_amount", "outstanding_amount", "allocated_amount", - "exchange_rate" + "exchange_rate", + "exchange_gain_loss" ], "fields": [ { @@ -90,12 +91,19 @@ "fieldtype": "Link", "label": "Payment Term", "options": "Payment Term" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-10 11:25:47.144392", + "modified": "2021-04-21 13:30:11.605388", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c1cc092554d..b99d75ec496 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -451,6 +451,7 @@ class PurchaseInvoice(BuyingController): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 189260a29da..a04d082f199 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -974,6 +974,109 @@ class TestPurchaseInvoice(unittest.TestCase): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() + def test_gain_loss_with_advance_entry(self): + unlink_enabled = frappe.db.get_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice") + frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) + pay = frappe.get_doc({ + 'doctype': 'Payment Entry', + 'company': '_Test Company', + 'payment_type': 'Pay', + 'party_type': 'Supplier', + 'party': '_Test Supplier USD', + 'paid_to': '_Test Payable USD - _TC', + 'paid_from': 'Cash - _TC', + 'paid_amount': 70000, + 'target_exchange_rate': 70, + 'received_amount': 1000, + }) + pay.insert() + pay.submit() + + pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", + conversion_rate=75, rate=500, do_not_save=1, qty=1) + pi.cost_center = "_Test Cost Center - _TC" + pi.advances = [] + pi.append("advances", { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 1000, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70 + }) + pi.save() + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 37500.0], + ["_Test Payable USD - _TC", -40000.0], + ["Exchange Gain/Loss - _TC", 2500.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s + group by account order by account asc""", (pi.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", + conversion_rate=73, rate=500, do_not_save=1, qty=1) + pi_2.cost_center = "_Test Cost Center - _TC" + pi_2.advances = [] + pi_2.append("advances", { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 500, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70 + }) + pi_2.save() + pi_2.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 36500.0], + ["_Test Payable USD - _TC", -38000.0], + ["Exchange Gain/Loss - _TC", 1500.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s + group by account order by account asc""", (pi_2.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + expected_gle = [ + ["_Test Payable USD - _TC", 70000.0], + ["Cash - _TC", -70000.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s and is_cancelled=0 + group by account order by account asc""", (pay.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + pi.reload() + pi.cancel() + + pi_2.reload() + pi_2.cancel() + + pay.reload() + pay.cancel() + + frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled) + def test_purchase_invoice_advance_taxes(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json index 5801b17f66f..63dfff8921f 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -1,235 +1,127 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-03-08 15:36:46", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "creation": "2013-03-08 15:36:46", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "remarks", + "reference_row", + "col_break1", + "advance_amount", + "allocated_amount", + "exchange_gain_loss", + "ref_exchange_rate" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Type", - "length": 0, - "no_copy": 1, - "oldfieldname": "journal_voucher", - "oldfieldtype": "Link", - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "180px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "no_copy": 1, + "oldfieldname": "journal_voucher", + "oldfieldtype": "Link", + "options": "DocType", + "print_width": "180px", + "read_only": 1, "width": "180px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Reference Name", - "length": 0, - "no_copy": 1, - "options": "reference_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "no_copy": 1, + "options": "reference_type", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "remarks", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Remarks", - "length": 0, - "no_copy": 1, - "oldfieldname": "remarks", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 3, + "fieldname": "remarks", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Remarks", + "no_copy": 1, + "oldfieldname": "remarks", + "oldfieldtype": "Small Text", + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_row", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Row", - "length": 0, - "no_copy": 1, - "oldfieldname": "jv_detail_no", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "80px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_row", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Row", + "no_copy": 1, + "oldfieldname": "jv_detail_no", + "oldfieldtype": "Date", + "print_hide": 1, + "print_width": "80px", + "read_only": 1, "width": "80px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "advance_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Advance Amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "advance_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "advance_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Advance Amount", + "no_copy": 1, + "oldfieldname": "advance_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "100px", + "read_only": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Allocated Amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "allocated_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated Amount", + "no_copy": 1, + "oldfieldname": "allocated_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "100px", "width": "100px" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "ref_exchange_rate", + "fieldtype": "Float", + "label": "Reference Exchange Rate", + "non_negative": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2016-08-26 02:30:54.407138", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Invoice Advance", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-04-20 16:26:53.820530", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Invoice Advance", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 55a5b99907b..6d1f6249c13 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -840,6 +840,7 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json index 14bf4d81330..29422d68cf6 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json @@ -1,235 +1,128 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:27:41", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "creation": "2013-02-22 01:27:41", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "remarks", + "reference_row", + "col_break1", + "advance_amount", + "allocated_amount", + "exchange_gain_loss", + "ref_exchange_rate" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Type", - "length": 0, - "no_copy": 1, - "oldfieldname": "journal_voucher", - "oldfieldtype": "Link", - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "250px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "no_copy": 1, + "oldfieldname": "journal_voucher", + "oldfieldtype": "Link", + "options": "DocType", + "print_width": "250px", + "read_only": 1, "width": "250px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Reference Name", - "length": 0, - "no_copy": 1, - "options": "reference_type", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "no_copy": 1, + "options": "reference_type", + "print_hide": 1, + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "remarks", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Remarks", - "length": 0, - "no_copy": 1, - "oldfieldname": "remarks", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 3, + "fieldname": "remarks", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Remarks", + "no_copy": 1, + "oldfieldname": "remarks", + "oldfieldtype": "Small Text", + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_row", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Row", - "length": 0, - "no_copy": 1, - "oldfieldname": "jv_detail_no", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_row", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Row", + "no_copy": 1, + "oldfieldname": "jv_detail_no", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "120px", + "read_only": 1, "width": "120px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "advance_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Advance amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "advance_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "advance_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Advance amount", + "no_copy": 1, + "oldfieldname": "advance_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "120px", + "read_only": 1, "width": "120px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Allocated amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "allocated_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated amount", + "no_copy": 1, + "oldfieldname": "allocated_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "120px", "width": "120px" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "ref_exchange_rate", + "fieldtype": "Float", + "label": "Reference Exchange Rate", + "non_negative": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2016-08-26 02:36:10.718057", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Invoice Advance", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-06-04 20:25:49.832052", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Invoice Advance", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 66a9b601257..a8e4b153f81 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate + "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(), + "exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation } if d.voucher_detail_no: @@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): payment_entry.set_amounts() if d.difference_amount and d.difference_account: - payment_entry.set_gain_or_loss(account_details={ + account_details = { 'account': d.difference_account, 'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company', - payment_entry.company, "cost_center"), - 'amount': d.difference_amount - }) + payment_entry.company, "cost_center") + } + if d.difference_amount: + account_details['amount'] = d.difference_amount + + payment_entry.set_gain_or_loss(account_details=account_details) if not do_not_save: payment_entry.save(ignore_permissions=True) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1c086e9edcd..a9860ed2f05 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -124,6 +124,8 @@ class AccountsController(TransactionBase): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() + self.set_advance_gain_or_loss() + if self.is_return: self.validate_qty() else: @@ -584,15 +586,18 @@ class AccountsController(TransactionBase): allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) - self.append("advances", { + advance_row = { "doctype": self.doctype + " Advance", "reference_type": d.reference_type, "reference_name": d.reference_name, "reference_row": d.reference_row, "remarks": d.remarks, "advance_amount": flt(d.amount), - "allocated_amount": allocated_amount - }) + "allocated_amount": allocated_amount, + "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry + } + + self.append("advances", advance_row) def get_advance_entries(self, include_unallocated=True): if self.doctype == "Sales Invoice": @@ -650,6 +655,66 @@ class AccountsController(TransactionBase): "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.") .format(d.reference_name, d.against_order)) + def set_advance_gain_or_loss(self): + if not self.get("advances"): + return + + for d in self.get("advances"): + advance_exchange_rate = d.ref_exchange_rate + if (d.allocated_amount and self.conversion_rate != 1 + and self.conversion_rate != advance_exchange_rate): + + base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount + base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount + difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate + + d.exchange_gain_loss = difference + + def make_exchange_gain_loss_gl_entries(self, gl_entries): + if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: + for d in self.get("advances"): + if d.exchange_gain_loss: + party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer + party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to + party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer" + + gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') + account_currency = get_account_currency(gain_loss_account) + if account_currency != self.company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) + + # for purchase + dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' + # just reverse for sales? + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + + gl_entries.append( + self.get_gl_dict({ + "account": gain_loss_account, + "account_currency": account_currency, + "against": party, + dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center, + "project": self.project + }, item=d) + ) + + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + + gl_entries.append( + self.get_gl_dict({ + "account": party_account, + "party_type": party_type, + "party": party, + "against": gain_loss_account, + dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center, + "project": self.project + }, self.party_account_currency, item=self) + ) + def update_against_document_in_jv(self): """ Links invoice and advance voucher: @@ -690,7 +755,9 @@ class AccountsController(TransactionBase): if self.party_account_currency != self.company_currency else 1), 'grand_total': (self.base_grand_total if self.party_account_currency == self.company_currency else self.grand_total), - 'outstanding_amount': self.outstanding_amount + 'outstanding_amount': self.outstanding_amount, + 'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'), + 'exchange_gain_loss': flt(d.get('exchange_gain_loss')) }) lst.append(args) @@ -1289,6 +1356,8 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, party_account_field = "paid_from" if party_type == "Customer" else "paid_to" currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" payment_type = "Receive" if party_type == "Customer" else "Pay" + exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" + payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" @@ -1305,27 +1374,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, "Payment Entry" as reference_type, t1.name as reference_name, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency + t1.{0} as currency, t1.{4} as exchange_rate from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 where t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t2.reference_doctype = %s {2} order by t1.posting_date {3} - """.format(currency_field, party_account_field, reference_condition, limit_cond), + """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1) if include_unallocated: unallocated_payment_entries = frappe.db.sql(""" select "Payment Entry" as reference_type, name as reference_name, - remarks, unallocated_amount as amount + remarks, unallocated_amount as amount, {2} as exchange_rate from `tabPayment Entry` where {0} = %s and party_type = %s and party = %s and payment_type = %s and docstatus = 1 and unallocated_amount > 0 order by posting_date {1} - """.format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1) + """.format(party_account_field, limit_cond, exchange_rate_field), + (party_account, party_type, party, payment_type), as_dict=1) return list(payment_entries_against_order) + list(unallocated_payment_entries) From 0683337f146b583425be369b5e0035439f601b76 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Jul 2021 20:21:07 +0530 Subject: [PATCH 376/376] fix: incorrect response by variance & resolution by variance (#26173) --- erpnext/patches.txt | 1 + .../v13_0/update_response_by_variance.py | 31 +++++++++++++++ .../service_level_agreement.js | 4 +- .../service_level_agreement.json | 2 +- .../service_level_agreement.py | 4 +- .../test_service_level_agreement.py | 38 ++++++++++++++++++- .../service_level_priority.json | 8 ++-- 7 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 erpnext/patches/v13_0/update_response_by_variance.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 986b0c5711e..c93f7a7ed91 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -290,5 +290,6 @@ erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold +erpnext.patches.v13_0.update_response_by_variance erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details diff --git a/erpnext/patches/v13_0/update_response_by_variance.py b/erpnext/patches/v13_0/update_response_by_variance.py new file mode 100644 index 00000000000..ef4d9763837 --- /dev/null +++ b/erpnext/patches/v13_0/update_response_by_variance.py @@ -0,0 +1,31 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists('DocType', 'Issue') and frappe.db.count('Issue'): + invalid_issues = frappe.get_all('Issue', { + 'first_responded_on': ['is', 'set'], + 'response_by_variance': ['<', 0] + }, ["name", "response_by_variance", "timestampdiff(Second, `first_responded_on`, `response_by`) as variance"]) + + # issues which has response_by_variance set as -ve + # but diff between first_responded_on & response_by is +ve i.e SLA isn't failed + invalid_issues = [d for d in invalid_issues if d.get('variance') > 0] + + for issue in invalid_issues: + frappe.db.set_value('Issue', issue.get('name'), 'response_by_variance', issue.get('variance'), update_modified=False) + + invalid_issues = frappe.get_all('Issue', { + 'resolution_date': ['is', 'set'], + 'resolution_by_variance': ['<', 0] + }, ["name", "resolution_by_variance", "timestampdiff(Second, `resolution_date`, `resolution_by`) as variance"]) + + # issues which has resolution_by_variance set as -ve + # but diff between resolution_date & resolution_by is +ve i.e SLA isn't failed + invalid_issues = [d for d in invalid_issues if d.get('variance') > 0] + + for issue in invalid_issues: + frappe.db.set_value('Issue', issue.get('name'), 'resolution_by_variance', issue.get('variance'), update_modified=False) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 308bce48dfb..ae2080c3b53 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -5,15 +5,15 @@ frappe.ui.form.on('Service Level Agreement', { setup: function(frm) { if (cint(frm.doc.apply_sla_for_resolution) === 1) { frm.get_field('priorities').grid.editable_fields = [ - {fieldname: 'priority', columns: 1}, {fieldname: 'default_priority', columns: 1}, + {fieldname: 'priority', columns: 2}, {fieldname: 'response_time', columns: 2}, {fieldname: 'resolution_time', columns: 2} ]; } else { frm.get_field('priorities').grid.editable_fields = [ - {fieldname: 'priority', columns: 1}, {fieldname: 'default_priority', columns: 1}, + {fieldname: 'priority', columns: 2}, {fieldname: 'response_time', columns: 3}, ]; } diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index de3389aa422..ef14b298968 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:SLA-{document_type}-{service_level}-{####}", + "autoname": "format:SLA-{document_type}-{service_level}", "creation": "2018-12-26 21:08:15.448812", "doctype": "DocType", "editable_grid": 1, diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 60e5fbe80ee..8739cb2364c 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -797,7 +797,7 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if meta.has_field("response_by_variance"): + if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) @@ -805,7 +805,7 @@ def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - if meta.has_field("resolution_by_variance"): + if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 7c18a6577fa..865fadc97c2 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -217,6 +217,42 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') + def test_changing_of_variance_after_response(self): + # create lead + doctype = "Lead" + lead_sla = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, + doctype=doctype, + sla_fulfilled_on=[{"status": "Replied"}], + apply_sla_for_resolution=0 + ) + creation = datetime.datetime(2019, 3, 4, 12, 0) + lead = make_lead(creation=creation, index=2) + self.assertEqual(lead.service_level_agreement, lead_sla.name) + + # set lead as replied to set first responded on + frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30) + lead.reload() + lead.status = 'Replied' + lead.save() + lead.reload() + self.assertEqual(lead.agreement_status, 'Fulfilled') + + # check response_by_variance + self.assertEqual(lead.first_responded_on, frappe.flags.current_time) + self.assertEqual(lead.response_by_variance, 1800.0) + + # make a change on the document & + # check response_by_variance is unchanged + frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30) + lead.status = 'Open' + lead.save() + lead.reload() + self.assertEqual(lead.response_by_variance, 1800.0) + def tearDown(self): for d in frappe.get_all("Service Level Agreement"): frappe.delete_doc("Service Level Agreement", d.name, force=1) @@ -249,7 +285,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "doctype": "Service Level Agreement", "enabled": 1, "document_type": doctype, - "service_level": "__Test Service Level", + "service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"), "default_service_level_agreement": default_service_level_agreement, "default_priority": "Medium", "holiday_list": holiday_list, diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json index 0367fc6d887..b410fe66604 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -5,9 +5,9 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "priority", - "cb_01", "default_priority", + "cb_01", + "priority", "sb_00", "response_time", "cb_00", @@ -15,7 +15,7 @@ ], "fields": [ { - "columns": 1, + "columns": 2, "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, @@ -64,7 +64,7 @@ ], "istable": 1, "links": [], - "modified": "2021-05-29 19:52:51.733248", + "modified": "2021-06-21 12:00:58.089962", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority",