From 5e98679f918618dd5bb31f432fc260f78c6409b9 Mon Sep 17 00:00:00 2001 From: Komal-Saraf0609 <81952590+Komal-Saraf0609@users.noreply.github.com> Date: Thu, 30 Mar 2023 08:03:55 +0530 Subject: [PATCH 01/22] fix: enabling lead even after "Opportunity" created against it (#34627) * fix: enabling lead even after "Opportunity" created against it * chore: Linting Issues --------- Co-authored-by: Komal Saraf Co-authored-by: Deepesh Garg (cherry picked from commit ad11934d39f0dca68630f4231031f8965fc74cf6) # Conflicts: # erpnext/patches.txt # erpnext/patches/v14_0/enable_all_leads.py --- erpnext/crm/doctype/opportunity/opportunity.py | 5 ----- erpnext/patches.txt | 5 +++++ erpnext/patches/v14_0/enable_all_leads.py | 8 ++++++++ 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 erpnext/patches/v14_0/enable_all_leads.py diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index f4b6e910ed1..6a5fead0f82 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -33,7 +33,6 @@ class Opportunity(TransactionBase, CRMNote): def after_insert(self): if self.opportunity_from == "Lead": frappe.get_doc("Lead", self.party_name).set_status(update=True) - self.disable_lead() link_open_tasks(self.opportunity_from, self.party_name, self) link_open_events(self.opportunity_from, self.party_name, self) @@ -119,10 +118,6 @@ class Opportunity(TransactionBase, CRMNote): prospect.flags.ignore_mandatory = True prospect.save() - def disable_lead(self): - if self.opportunity_from == "Lead": - frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1}) - def make_new_lead_if_required(self): """Set lead against new opportunity""" if (not self.get("party_name")) and self.contact_email: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5803f46dea3..c5a88e33187 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -328,3 +328,8 @@ erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger +<<<<<<< HEAD +======= +execute:frappe.delete_doc_if_exists("Report", "Tax Detail") +erpnext.patches.v15_0.enable_all_leads +>>>>>>> ad11934d39 (fix: enabling lead even after "Opportunity" created against it (#34627)) diff --git a/erpnext/patches/v14_0/enable_all_leads.py b/erpnext/patches/v14_0/enable_all_leads.py new file mode 100644 index 00000000000..c1f2b47b5bd --- /dev/null +++ b/erpnext/patches/v14_0/enable_all_leads.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + lead = frappe.qb.DocType("Lead") + frappe.qb.update(lead).set(lead.disabled, 0).set(lead.docstatus, 0).where( + lead.disabled == 1 and lead.docstatus == 1 + ).run() From bc94358e98ca5dbffcf652de873fa25df657eb5b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 30 Mar 2023 12:15:56 +0530 Subject: [PATCH 02/22] chore: resolve conflicts --- erpnext/patches.txt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c5a88e33187..82c8b06555b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -326,10 +326,7 @@ erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link -# below migration patches should always run last -erpnext.patches.v14_0.migrate_gl_to_payment_ledger -<<<<<<< HEAD -======= execute:frappe.delete_doc_if_exists("Report", "Tax Detail") erpnext.patches.v15_0.enable_all_leads ->>>>>>> ad11934d39 (fix: enabling lead even after "Opportunity" created against it (#34627)) +# below migration patches should always run last +erpnext.patches.v14_0.migrate_gl_to_payment_ledger From eb3e6ff145b73e974cf1eeb792e488ae11408697 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 3 May 2023 12:21:23 +0530 Subject: [PATCH 03/22] Update patches.txt --- erpnext/patches.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8cdc57752d1..f55f42ed469 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -326,7 +326,6 @@ erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link -execute:frappe.delete_doc_if_exists("Report", "Tax Detail") erpnext.patches.v15_0.enable_all_leads execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) # below migration patches should always run last From d306bb080a1728adbab44f4236abadad7135a92e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 3 May 2023 12:42:36 +0530 Subject: [PATCH 04/22] Update patches.txt --- erpnext/patches.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f55f42ed469..f010f1a1be6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -326,7 +326,7 @@ erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link -erpnext.patches.v15_0.enable_all_leads +erpnext.patches.v14_0.enable_all_leads execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger From d1a91177e551850a65aaa846fec58527e87b68a9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 3 May 2023 18:06:47 +0530 Subject: [PATCH 05/22] feat: reserve qty against production plan raw materials in BIN (cherry picked from commit 06e91e758f18ea463351b5c6b6e5ab5204a4705c) --- .../material_request_plan_item.json | 9 +++- .../production_plan/production_plan.js | 10 ++--- .../production_plan/production_plan.py | 44 +++++++++++++++++++ .../production_plan/test_production_plan.py | 21 +++++++++ .../doctype/work_order/work_order.py | 30 +++++++++---- erpnext/stock/doctype/bin/bin.json | 9 +++- erpnext/stock/doctype/bin/bin.py | 30 ++++++++++++- .../stock_projected_qty.py | 9 ++++ 8 files changed, 143 insertions(+), 19 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index 8c61d545b81..09bf1d8a736 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -16,6 +16,7 @@ "column_break_4", "quantity", "uom", + "conversion_factor", "projected_qty", "reserved_qty_for_production", "safety_stock", @@ -169,11 +170,17 @@ "label": "Qty As Per BOM", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2022-11-26 14:59:25.879631", + "modified": "2023-05-03 12:43:29.895754", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 62715e65653..ab7aa52bb7a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -336,10 +336,6 @@ frappe.ui.form.on('Production Plan', { }, get_items_for_material_requests(frm, warehouses) { - let set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse', - 'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty', - 'reserved_qty_for_production', 'material_request_type']; - frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", freeze: true, @@ -352,11 +348,11 @@ frappe.ui.form.on('Production Plan', { frm.set_value('mr_items', []); r.message.forEach(row => { let d = frm.add_child('mr_items'); - set_fields.forEach(field => { - if (row[field]) { + for (let field in row) { + if (field !== 'name') { d[field] = row[field]; } - }); + } }); } refresh_field('mr_items'); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0cc0f80cf1a..df50cbf2695 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -28,6 +28,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.utils import get_or_make_bin from erpnext.utilities.transaction_base import validate_uom_is_integer @@ -398,9 +399,20 @@ class ProductionPlan(Document): self.set_status() self.db_set("status", self.status) + def on_submit(self): + self.update_bin_qty() + def on_cancel(self): self.db_set("status", "Cancelled") self.delete_draft_work_order() + self.update_bin_qty() + + def update_bin_qty(self): + for d in self.mr_items: + if d.warehouse: + bin_name = get_or_make_bin(d.item_code, d.warehouse) + bin = frappe.get_doc("Bin", bin_name, for_update=True) + bin.update_reserved_qty_for_production_plan() def delete_draft_work_order(self): for d in frappe.get_all( @@ -1068,6 +1080,7 @@ def get_material_request_items( "item_code": row.item_code, "item_name": row.item_name, "quantity": required_qty / conversion_factor, + "conversion_factor": conversion_factor, "required_bom_qty": total_qty, "stock_uom": row.get("stock_uom"), "warehouse": warehouse @@ -1474,3 +1487,34 @@ def set_default_warehouses(row, default_warehouses): for field in ["wip_warehouse", "fg_warehouse"]: if not row.get(field): row[field] = default_warehouses.get(field) + + +def get_reserved_qty_for_production_plan(item_code, warehouse): + from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production + + table = frappe.qb.DocType("Production Plan") + child = frappe.qb.DocType("Material Request Plan Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) + .where( + (table.docstatus == 1) + & (child.item_code == item_code) + & (child.warehouse == warehouse) + & (table.status.notin(["Completed", "Closed"])) + ) + ).run() + + if not query: + return 0.0 + + reserved_qty_for_production_plan = flt(query[0][0]) + + reserved_qty_for_production = flt( + get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True) + ) + + return reserved_qty_for_production_plan - reserved_qty_for_production diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2bf14c24cf3..91864d09dbd 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -868,6 +868,27 @@ class TestProductionPlan(FrappeTestCase): for item_code in mr_items: self.assertTrue(item_code in validate_mr_items) + def test_resered_qty_for_production_plan_for_material_requests(self): + from erpnext.stock.utils import get_or_make_bin + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan(item_code="Test Production Item 1") + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty - before_qty, 1) + + pln = frappe.get_doc("Production Plan", pln.name) + pln.cancel() + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty, before_qty) + def create_production_plan(**args): """ diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 66b871c746f..75845226a65 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -558,12 +558,19 @@ class WorkOrder(Document): and self.production_plan_item and not self.production_plan_sub_assembly_item ): - qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0 + table = frappe.qb.DocType("Work Order") - if self.docstatus == 1: - qty += self.qty - elif self.docstatus == 2: - qty -= self.qty + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty)) + .where( + (table.production_plan == self.production_plan) + & (table.production_plan_item == self.production_plan_item) + & (table.docstatus == 1) + ) + ).run() + + qty = flt(query[0][0]) if query else 0 frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) @@ -1476,12 +1483,14 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): return doc -def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: +def get_reserved_qty_for_production( + item_code: str, warehouse: str, check_production_plan: bool = False +) -> float: """Get total reserved quantity for any item in specified warehouse""" wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") - return ( + query = ( frappe.qb.from_(wo) .from_(wo_item) .select( @@ -1502,7 +1511,12 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: | (wo_item.required_qty > wo_item.consumed_qty) ) ) - ).run()[0][0] or 0.0 + ) + + if check_production_plan: + query = query.where(wo.production_plan.isnotnull()) + + return query.run()[0][0] or 0.0 @frappe.whitelist() diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index d822f4a6095..a11572776a2 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -15,6 +15,7 @@ "projected_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", + "reserved_qty_for_production_plan", "ma_rate", "stock_uom", "fcfs_rate", @@ -165,13 +166,19 @@ "oldfieldname": "stock_value", "oldfieldtype": "Currency", "read_only": 1 + }, + { + "fieldname": "reserved_qty_for_production_plan", + "fieldtype": "Float", + "label": "Reserved Qty for Production Plan", + "read_only": 1 } ], "hide_toolbar": 1, "idx": 1, "in_create": 1, "links": [], - "modified": "2022-03-30 07:22:23.868602", + "modified": "2023-05-02 23:26:21.806965", "modified_by": "Administrator", "module": "Stock", "name": "Bin", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 72654e6f816..5abea9e69fe 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -24,8 +24,30 @@ class Bin(Document): - flt(self.reserved_qty) - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract) + - flt(self.reserved_qty_for_production_plan) ) + def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False): + """Update qty reserved for production from Production Plan tables + in open production plan""" + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_production_plan, + ) + + self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan( + self.item_code, self.warehouse + ) + + self.db_set( + "reserved_qty_for_production_plan", + flt(self.reserved_qty_for_production_plan), + update_modified=True, + ) + + if not skip_project_qty_update: + self.set_projected_qty() + self.db_set("projected_qty", self.projected_qty, update_modified=True) + def update_reserved_qty_for_production(self): """Update qty reserved for production from Production Item tables in open work orders""" @@ -35,11 +57,13 @@ class Bin(Document): self.item_code, self.warehouse ) - self.set_projected_qty() - self.db_set( "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True ) + + self.update_reserved_qty_for_production_plan(skip_project_qty_update=True) + + self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): @@ -141,6 +165,7 @@ def get_bin_details(bin_name): "planned_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", + "reserved_qty_for_production_plan", ], as_dict=1, ) @@ -188,6 +213,7 @@ def update_qty(bin_name, args): - flt(reserved_qty) - flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract) + - flt(bin_details.reserved_qty_for_production_plan) ) frappe.db.set_value( diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index f477d8f08fd..31c756da822 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -76,6 +76,7 @@ def execute(filters=None): bin.ordered_qty, bin.reserved_qty, bin.reserved_qty_for_production, + bin.reserved_qty_for_production_plan, bin.reserved_qty_for_sub_contract, reserved_qty_for_pos, bin.projected_qty, @@ -173,6 +174,13 @@ def get_columns(): "width": 100, "convertible": "qty", }, + { + "label": _("Reserved for Production Plan"), + "fieldname": "reserved_qty_for_production_plan", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, { "label": _("Reserved for Sub Contracting"), "fieldname": "reserved_qty_for_sub_contract", @@ -232,6 +240,7 @@ def get_bin_list(filters): bin.reserved_qty, bin.reserved_qty_for_production, bin.reserved_qty_for_sub_contract, + bin.reserved_qty_for_production_plan, bin.projected_qty, ) .orderby(bin.item_code, bin.warehouse) From bf6e1b67a5870af7cf7881f409b1dd35b0f4e7f7 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 3 May 2023 23:22:59 +0530 Subject: [PATCH 06/22] fix: over production percentage not considered in validation (cherry picked from commit a84d0af81e1dbba309a76471cd98e91bf6441d22) --- .../doctype/job_card/job_card.py | 19 +++++++++++++++++-- .../doctype/work_order/test_work_order.py | 8 ++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index f89951619e0..877362dcba4 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -87,6 +87,12 @@ class JobCard(Document): frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty") ) + over_production_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) + + wo_qty = wo_qty + (wo_qty * over_production_percentage / 100) + job_card_qty = frappe.get_all( "Job Card", fields=["sum(for_quantity)"], @@ -101,8 +107,17 @@ class JobCard(Document): job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0 if job_card_qty and ((job_card_qty - completed_qty) > wo_qty): - msg = f"""Job Card quantity cannot be greater than - Work Order quantity for the operation {self.operation}""" + form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings") + + msg = f""" + Qty To Manufacture in the job card + cannot be greater than Qty To Manufacture in the + work order for the operation {bold(self.operation)}. +

Solution: Either you can reduce the + Qty To Manufacture in the job card or set the + 'Overproduction Percentage For Work Order' + in the {form_link}.""" + frappe.throw(_(msg), title=_("Extra Job Card Quantity")) def set_sub_operations(self): diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 540b7dc9ea6..bb53c8c225c 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1649,6 +1649,14 @@ class TestWorkOrder(FrappeTestCase): job_card2 = frappe.copy_doc(job_card_doc) self.assertRaises(frappe.ValidationError, job_card2.save) + frappe.db.set_single_value( + "Manufacturing Settings", "overproduction_percentage_for_work_order", 100 + ) + + job_card2 = frappe.copy_doc(job_card_doc) + job_card2.time_logs = [] + job_card2.save() + def prepare_data_for_workstation_type_check(): from erpnext.manufacturing.doctype.operation.test_operation import make_operation From f5f49024945940988fedbb52a07b18d4a3952fef Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 4 May 2023 15:38:35 +0530 Subject: [PATCH 07/22] fix: internal transfer condition (cherry picked from commit b5a2ccf21d0b0ca9fb4c8d958d29d60dcfb5ecb2) --- .../doctype/purchase_receipt/purchase_receipt.py | 2 +- erpnext/stock/stock_ledger.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 530427328a8..3373d8ac8c5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -379,7 +379,7 @@ class PurchaseReceipt(BuyingController): ) outgoing_amount = d.base_net_amount - if self.is_internal_supplier and d.valuation_rate: + if self.is_internal_transfer() and d.valuation_rate: outgoing_amount = abs( frappe.db.get_value( "Stock Ledger Entry", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8b517bf1e0f..103ed4ac3d0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -556,7 +556,7 @@ class update_entries_after(object): sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no and sle.actual_qty < 0 - and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier") + and is_internal_transfer(sle) ): sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) @@ -679,7 +679,7 @@ class update_entries_after(object): elif ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no - and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier") + and is_internal_transfer(sle) ): rate = get_incoming_rate_for_inter_company_transfer(sle) else: @@ -1609,3 +1609,15 @@ def get_incoming_rate_for_inter_company_transfer(sle) -> float: ) return rate + + +def is_internal_transfer(sle): + data = frappe.get_cached_value( + sle.voucher_type, + sle.voucher_no, + ["is_internal_supplier", "represents_company", "company"], + as_dict=True, + ) + + if data.is_internal_supplier and data.represents_company == data.company: + return True From 33cd14f859dfd8959048aac59adb9fdffdd216b8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 4 May 2023 16:49:14 +0530 Subject: [PATCH 08/22] feat: configuration to notify reposting errors to specific role (cherry picked from commit c7b62011db5561ed2a3efacec26eb55b3f38eb43) --- .../repost_item_valuation.py | 15 ++++++-- .../stock_reposting_settings.json | 18 ++++++++-- .../test_stock_reposting_settings.py | 35 +++++++++++++++++-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index aabc6fcfe30..50534605feb 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -300,9 +300,7 @@ def _get_directly_dependent_vouchers(doc): def notify_error_to_stock_managers(doc, traceback): - recipients = get_users_with_role("Stock Manager") - if not recipients: - recipients = get_users_with_role("System Manager") + recipients = get_recipients() subject = _("Error while reposting item valuation") message = ( @@ -319,6 +317,17 @@ def notify_error_to_stock_managers(doc, traceback): frappe.sendmail(recipients=recipients, subject=subject, message=message) +def get_recipients(): + role = ( + frappe.db.get_single_value("Stock Reposting Settings", "notify_reposting_error_to_role") + or "Stock Manager" + ) + + recipients = get_users_with_role(role) + + return recipients + + def repost_entries(): """ Reposts 'Repost Item Valuation' entries in queue. diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json index 0facae8d3b8..7c712ce2258 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -12,7 +12,9 @@ "start_time", "end_time", "limits_dont_apply_on", - "item_based_reposting" + "item_based_reposting", + "errors_notification_section", + "notify_reposting_error_to_role" ], "fields": [ { @@ -52,12 +54,23 @@ "fieldname": "item_based_reposting", "fieldtype": "Check", "label": "Use Item based reposting" + }, + { + "fieldname": "notify_reposting_error_to_role", + "fieldtype": "Link", + "label": "Notify Reposting Error to Role", + "options": "Role" + }, + { + "fieldname": "errors_notification_section", + "fieldtype": "Section Break", + "label": "Errors Notification" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-11-02 01:22:45.155841", + "modified": "2023-05-04 16:14:29.080697", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reposting Settings", @@ -76,5 +89,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py index fad74d355cf..a6dc72d7a42 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py @@ -1,9 +1,40 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe + +from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import get_recipients + class TestStockRepostingSettings(unittest.TestCase): - pass + def test_notify_reposting_error_to_role(self): + role = "Notify Reposting Role" + + if not frappe.db.exists("Role", role): + frappe.get_doc({"doctype": "Role", "role_name": role}).insert(ignore_permissions=True) + + user = "notify_reposting_error@test.com" + if not frappe.db.exists("User", user): + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": "Test", + "language": "en", + "time_zone": "Asia/Kolkata", + "send_welcome_email": 0, + "roles": [{"role": role}], + } + ).insert(ignore_permissions=True) + + frappe.db.set_single_value("Stock Reposting Settings", "notify_reposting_error_to_role", "") + + users = get_recipients() + self.assertFalse(user in users) + + frappe.db.set_single_value("Stock Reposting Settings", "notify_reposting_error_to_role", role) + + users = get_recipients() + self.assertTrue(user in users) From b0c042de1b78e09344c67530306220bac7b82faa Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 4 May 2023 18:15:25 +0530 Subject: [PATCH 09/22] fix: not allow to transfer excess materials against the job card (cherry picked from commit 8167b24219519793a672db2f05055dca1e6a348a) --- .../doctype/job_card/test_job_card.py | 36 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 19 ++++++++++ 2 files changed, 55 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 4d2dab73e3a..61766a67511 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -272,6 +272,42 @@ class TestJobCard(FrappeTestCase): transfer_entry_2.insert() self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit) + @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0}) + def test_job_card_excess_material_transfer_with_no_reference(self): + + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" + + self.generate_required_stock(self.work_order) + + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) + + # fully transfer both RMs + transfer_entry_1 = make_stock_entry_from_jc(job_card_name) + row = transfer_entry_1.items[0] + + # Add new row without reference of the job card item + transfer_entry_1.append( + "items", + { + "item_code": row.item_code, + "item_name": row.item_name, + "item_group": row.item_group, + "qty": row.qty, + "uom": row.uom, + "conversion_factor": row.conversion_factor, + "stock_uom": row.stock_uom, + "basic_rate": row.basic_rate, + "basic_amount": row.basic_amount, + "expense_account": row.expense_account, + "cost_center": row.cost_center, + "s_warehouse": row.s_warehouse, + "t_warehouse": row.t_warehouse, + }, + ) + + self.assertRaises(frappe.ValidationError, transfer_entry_1.insert) + def test_job_card_partial_material_transfer(self): "Test partial material transfer against Job Card" self.transfer_material_against = "Job Card" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5d6e45013d8..2e89078440f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -130,6 +130,7 @@ class StockEntry(StockController): self.validate_fg_completed_qty() self.validate_difference_account() self.set_job_card_data() + self.validate_job_card_item() self.set_purpose_for_stock_entry() self.clean_serial_nos() self.validate_duplicate_serial_no() @@ -214,6 +215,24 @@ class StockEntry(StockController): self.from_bom = 1 self.bom_no = data.bom_no + def validate_job_card_item(self): + if not self.job_card: + return + + if cint(frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")): + return + + for row in self.items: + if row.job_card_item: + continue + + msg = f"""Row #{0}: The job card item reference + is missing. Kindly create the stock entry + from the job card. If you have added the row manually + then you won't be able to add job card item reference.""" + + frappe.throw(_(msg)) + def validate_work_order_status(self): pro_doc = frappe.get_doc("Work Order", self.work_order) if pro_doc.status == "Completed": From ed5f39c2c2f7cb960e0cccac80b252cdce1077cf Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Sat, 6 May 2023 16:38:59 +0530 Subject: [PATCH 10/22] fix: handle empty FBs properly in TB and GL [v14] (#35189) fix: handle empty FBs properly in TB and GL --- erpnext/accounts/report/financial_statements.py | 12 ++++++++---- .../accounts/report/general_ledger/general_ledger.py | 8 ++++---- .../accounts/report/trial_balance/trial_balance.py | 10 ++++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 86fdaaa924a..fc3285be3db 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -500,14 +500,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters): _("To use a different finance book, please uncheck 'Include Default Book Entries'") ) else: - additional_conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)") + additional_conditions.append( + "(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" + ) else: - additional_conditions.append("(finance_book in (%(company_fb)s) OR finance_book IS NULL)") + additional_conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)") else: if filters.get("finance_book"): - additional_conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)") + additional_conditions.append( + "(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" + ) else: - additional_conditions.append("(finance_book IS NULL)") + additional_conditions.append("(finance_book in ('') OR finance_book IS NULL)") if accounting_dimensions: for dimension in accounting_dimensions: diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 6a4f394ebc8..227d9b68ce6 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -253,14 +253,14 @@ def get_conditions(filters): _("To use a different finance book, please uncheck 'Include Default Book Entries'") ) else: - conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)") + conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") else: - conditions.append("(finance_book in (%(company_fb)s) OR finance_book IS NULL)") + conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)") else: if filters.get("finance_book"): - conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)") + conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") else: - conditions.append("(finance_book IS NULL)") + conditions.append("(finance_book in ('') OR finance_book IS NULL)") if not filters.get("show_cancelled_entries"): conditions.append("is_cancelled = 0") diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d2300cd1238..3bcd1bc68d0 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -166,14 +166,16 @@ def get_rootwise_opening_balances(filters, report_type): _("To use a different finance book, please uncheck 'Include Default Book Entries'") ) else: - additional_conditions += " AND (finance_book in (%(finance_book)s) OR finance_book IS NULL)" + additional_conditions += ( + " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" + ) else: - additional_conditions += " AND (finance_book in (%(company_fb)s) OR finance_book IS NULL)" + additional_conditions += " AND (finance_book in (%(company_fb)s, '') OR finance_book IS NULL)" else: if filters.get("finance_book"): - additional_conditions += " AND (finance_book in (%(finance_book)s) OR finance_book IS NULL)" + additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" else: - additional_conditions += " AND (finance_book IS NULL)" + additional_conditions += " AND (finance_book in ('') OR finance_book IS NULL)" accounting_dimensions = get_accounting_dimensions(as_list=False) From 5c38645560281b47fd86f2e8efe3873ac6f44b12 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 6 May 2023 20:30:53 +0530 Subject: [PATCH 11/22] fix: incorrect fg item quantity in subcontracted PO (cherry picked from commit af16fbb0a301514ab9a890a5f87571ee739539e2) --- .../production_plan/production_plan.py | 1 + .../production_plan/test_production_plan.py | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index df50cbf2695..f9e68b916f8 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -587,6 +587,7 @@ class ProductionPlan(Document): "production_plan_sub_assembly_item": row.name, "bom": row.bom_no, "production_plan": self.name, + "fg_item_qty": row.qty, } for field in [ diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 91864d09dbd..4648d896cee 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -307,6 +307,43 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier") + def test_production_plan_for_subcontracting_po(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + bom_tree_1 = {"Test Laptop 1": {"Test Motherboard 1": {"Test Motherboard Wires 1": {}}}} + create_nested_bom(bom_tree_1, prefix="") + + item_doc = frappe.get_doc("Item", "Test Motherboard 1") + company = "_Test Company" + + item_doc.is_sub_contracted_item = 1 + for row in item_doc.item_defaults: + if row.company == company and not row.default_supplier: + row.default_supplier = "_Test Supplier" + + if not item_doc.item_defaults: + item_doc.append("item_defaults", {"company": company, "default_supplier": "_Test Supplier"}) + + item_doc.save() + + plan = create_production_plan( + item_code="Test Laptop 1", planned_qty=10, use_multi_level_bom=1, do_not_submit=True + ) + plan.get_sub_assembly_items() + plan.set_default_supplier_for_subcontracting_order() + plan.submit() + + self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier") + plan.make_work_order() + + po = frappe.db.get_value("Purchase Order Item", {"production_plan": plan.name}, "parent") + po_doc = frappe.get_doc("Purchase Order", po) + self.assertEqual(po_doc.supplier, "_Test Supplier") + self.assertEqual(po_doc.items[0].qty, 10.0) + self.assertEqual(po_doc.items[0].fg_item_qty, 10.0) + self.assertEqual(po_doc.items[0].fg_item_qty, 10.0) + self.assertEqual(po_doc.items[0].fg_item, "Test Motherboard 1") + def test_production_plan_combine_subassembly(self): """ Test combining Sub assembly items belonging to the same BOM in Prod Plan. From 545f95616071b6865b69ddcecdceae5dba7dd03e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 6 May 2023 20:09:15 +0530 Subject: [PATCH 12/22] fix: pick the in progress reposting entries first (cherry picked from commit c8a4791d9b8753fdb715ac8e164b036290229c92) --- .../doctype/repost_item_valuation/repost_item_valuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 50534605feb..d3bcab76ab5 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -353,7 +353,7 @@ def get_repost_item_valuation_entries(): return frappe.db.sql( """ SELECT name from `tabRepost Item Valuation` WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 - ORDER BY timestamp(posting_date, posting_time) asc, creation asc + ORDER BY timestamp(posting_date, posting_time) asc, creation asc, status asc """, now(), as_dict=1, From 42f5888426f49bb6f3b9f8e12e3539f3d03e50ec Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 7 May 2023 20:24:50 +0530 Subject: [PATCH 13/22] fix: error regarding accepted and supplier warehouse (cherry picked from commit 15f5f98858875a93f8f734ab92ef393c63442dd3) --- .../doctype/purchase_invoice/purchase_invoice.json | 3 ++- erpnext/buying/doctype/purchase_order/purchase_order.json | 3 ++- .../stock/doctype/material_request/material_request.json | 3 ++- .../material_request_item/material_request_item.json | 8 +++++--- .../stock/doctype/purchase_receipt/purchase_receipt.json | 3 ++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 719ef7d4c44..5a08f045b8c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1369,6 +1369,7 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", + "ignore_user_permissions": 1, "width": "50px" }, { @@ -1572,7 +1573,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-04-28 12:57:50.832598", + "modified": "2023-04-29 12:57:50.832598", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index ff08ddd33dc..c51c6edf1da 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1171,6 +1171,7 @@ "depends_on": "is_internal_supplier", "fieldname": "set_from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set From Warehouse", "options": "Warehouse" }, @@ -1271,7 +1272,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-04-14 16:42:29.448464", + "modified": "2023-05-07 20:18:09.196799", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 413c373e0e4..ae39470d1dc 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -280,6 +280,7 @@ { "fieldname": "set_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_list_view": 1, "label": "Set Target Warehouse", "options": "Warehouse" @@ -355,7 +356,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2022-09-27 17:58:26.366469", + "modified": "2023-05-07 20:17:29.108095", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index dd66cfff8be..770dacdd19c 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -419,6 +419,7 @@ "depends_on": "eval:parent.material_request_type == \"Material Transfer\"", "fieldname": "from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Source Warehouse", "options": "Warehouse" }, @@ -451,16 +452,16 @@ "fieldname": "job_card_item", "fieldtype": "Data", "hidden": 1, + "label": "Job Card Item", "no_copy": 1, - "print_hide": 1, - "label": "Job Card Item" + "print_hide": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-03-10 18:42:42.705190", + "modified": "2023-05-07 20:23:31.250252", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", @@ -469,5 +470,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 8f043585b89..dc61ec4d243 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1113,6 +1113,7 @@ "depends_on": "eval: doc.is_internal_supplier", "fieldname": "set_from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set From Warehouse", "options": "Warehouse" }, @@ -1238,7 +1239,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:40:32.447752", + "modified": "2023-05-07 20:18:25.458185", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From 72255fae8047c9eb321224ae33bf54458fb7b0d1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 6 May 2023 10:40:41 +0530 Subject: [PATCH 14/22] fix: child acc will inherit acc currency if explicitly specified (cherry picked from commit abe691c03d833e9d2ed230daf2640da040b1be1e) --- erpnext/accounts/doctype/account/account.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 6635d3e733b..bbe4c54a71a 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -201,8 +201,11 @@ class Account(NestedSet): ) def validate_account_currency(self): + self.currency_explicitly_specified = True + if not self.account_currency: self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency") + self.currency_explicitly_specified = False gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency") @@ -248,8 +251,10 @@ class Account(NestedSet): { "company": company, # parent account's currency should be passed down to child account's curreny - # if it is None, it picks it up from default company currency, which might be unintended - "account_currency": erpnext.get_company_currency(company), + # if currency explicitly specified by user, child will inherit. else, default currency will be used. + "account_currency": self.account_currency + if self.currency_explicitly_specified + else erpnext.get_company_currency(company), "parent_account": parent_acc_name_map[company], } ) From e451916803682b2a1db8061b53a585395d076af6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 8 May 2023 13:30:39 +0530 Subject: [PATCH 15/22] test: currency inheritance on child accounts (cherry picked from commit f6ea8fd8d7a98d19bc6ec0d43ae06ffe31d1e3ff) --- .../accounts/doctype/account/test_account.py | 55 +++++++++++++++++++ .../setup/doctype/company/test_records.json | 25 +++++++++ 2 files changed, 80 insertions(+) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 3a360c48c43..62303bd723f 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -5,10 +5,13 @@ import unittest import frappe +from frappe.test_runner import make_test_records from erpnext.accounts.doctype.account.account import merge_account, update_account_number from erpnext.stock import get_company_default_inventory_account, get_warehouse_account +test_dependencies = ["Company"] + class TestAccount(unittest.TestCase): def test_rename_account(self): @@ -188,6 +191,58 @@ class TestAccount(unittest.TestCase): frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5") + def test_account_currency_sync(self): + """ + In a parent->child company setup, child should inherit parent account currency if explicitly specified. + """ + + make_test_records("Company") + + frappe.local.flags.pop("ignore_root_company_validation", None) + + def create_bank_account(): + acc = frappe.new_doc("Account") + acc.account_name = "_Test Bank JPY" + + acc.parent_account = "Temporary Accounts - _TC6" + acc.company = "_Test Company 6" + return acc + + acc = create_bank_account() + # Explicitly set currency + acc.account_currency = "JPY" + acc.insert() + self.assertTrue( + frappe.db.exists( + { + "doctype": "Account", + "account_name": "_Test Bank JPY", + "account_currency": "JPY", + "company": "_Test Company 7", + } + ) + ) + + frappe.delete_doc("Account", "_Test Bank JPY - _TC6") + frappe.delete_doc("Account", "_Test Bank JPY - _TC7") + + acc = create_bank_account() + # default currency is used + acc.insert() + self.assertTrue( + frappe.db.exists( + { + "doctype": "Account", + "account_name": "_Test Bank JPY", + "account_currency": "USD", + "company": "_Test Company 7", + } + ) + ) + + frappe.delete_doc("Account", "_Test Bank JPY - _TC6") + frappe.delete_doc("Account", "_Test Bank JPY - _TC7") + def test_child_company_account_rename_sync(self): frappe.local.flags.pop("ignore_root_company_validation", None) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 19b6ef27ac3..e21bd2a3ce8 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -80,5 +80,30 @@ "chart_of_accounts": "Standard", "enable_perpetual_inventory": 1, "default_holiday_list": "_Test Holiday List" + }, + { + "abbr": "_TC6", + "company_name": "_Test Company 6", + "is_group": 1, + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "domain": "Manufacturing", + "chart_of_accounts": "Standard", + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 + }, + { + "abbr": "_TC7", + "company_name": "_Test Company 7", + "parent_company": "_Test Company 6", + "is_group": 1, + "country": "United States", + "default_currency": "USD", + "doctype": "Company", + "domain": "Manufacturing", + "chart_of_accounts": "Standard", + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 0 } ] From c73b76fdb63130a2e6445c5462dc0971506f4abf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 May 2023 16:40:38 +0530 Subject: [PATCH 16/22] fix: bypass flag in Customer Group wasn't effective (cherry picked from commit f9a4972cb621985dcd5e17460627931db0877e8a) --- erpnext/selling/doctype/customer/customer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index a8665c5d040..18fd8ff8ef7 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -665,11 +665,15 @@ def get_credit_limit(customer, company): if not credit_limit: customer_group = frappe.get_cached_value("Customer", customer, "customer_group") - credit_limit = frappe.db.get_value( + + result = frappe.db.get_values( "Customer Credit Limit", {"parent": customer_group, "parenttype": "Customer Group", "company": company}, - "credit_limit", + fieldname=["credit_limit", "bypass_credit_limit_check"], + as_dict=True, ) + if result and not result[0].bypass_credit_limit_check: + credit_limit = result[0].credit_limit if not credit_limit: credit_limit = frappe.get_cached_value("Company", company, "credit_limit") From f42225bc822a30dde2d4f114fb28a149cf595b99 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 4 May 2023 10:45:22 +0530 Subject: [PATCH 17/22] fix: fetch default sales team on Quotation -> Sales Order creation (cherry picked from commit 4d314369175fda677af6a5b545d46e26ce719665) --- erpnext/selling/doctype/quotation/quotation.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index fc66db20d29..693fc92ce93 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -286,6 +286,18 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.commission_rate = frappe.get_value( "Sales Partner", source.referral_sales_partner, "commission_rate" ) + + # sales team + for d in customer.get("sales_team"): + target.append( + "sales_team", + { + "sales_person": d.sales_person, + "allocated_percentage": d.allocated_percentage or None, + "commission_rate": d.commission_rate, + }, + ) + target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") From 08a4781de7a0f4ddf78bf8d6d581f41f11d828f9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 8 May 2023 16:42:12 +0530 Subject: [PATCH 18/22] chore: convert throw to msgprint (cherry picked from commit 73134d57bf38be2271514431461da781d89fa821) --- .../process_payment_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index ecb51ce1445..31660306db0 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -164,7 +164,7 @@ def trigger_reconciliation_for_queued_docs(): Fetch queued docs and start reconciliation process for each one """ if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"): - frappe.throw( + frappe.msgprint( _("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format( get_link_to_form("Accounts Settings", "Accounts Settings") ) From 362003ec5fcd449f0f2cdda42e84d312e76cabb3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 9 May 2023 12:42:17 +0530 Subject: [PATCH 19/22] fix: added search index to improve performance (cherry picked from commit 80ea22b56c75e072b3a9c855441736a3cabbe3f1) --- .../stock/doctype/stock_entry_detail/stock_entry_detail.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 fe81a87558c..6b1a8efc997 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -395,7 +395,8 @@ "no_copy": 1, "options": "Material Request", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "material_request_item", @@ -571,7 +572,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-01-03 14:51:16.575515", + "modified": "2023-05-09 12:41:18.210864", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From c0f9ff4995864ad344c2a9f72586e435475367e8 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 8 May 2023 12:26:47 +0000 Subject: [PATCH 20/22] fix: broken save on empty row existance (cherry picked from commit d9b231aa164dc72bafe2e51e30a17478f416f676) --- .../public/js/controllers/taxes_and_totals.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8efc47d18e5..fd961c4aaae 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -92,7 +92,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { _calculate_taxes_and_totals() { const is_quotation = this.frm.doc.doctype == "Quotation"; - this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items; + this.frm._items = is_quotation ? this.filtered_items() : this.frm.doc.items; this.validate_conversion_rate(); this.calculate_item_values(); @@ -125,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_item_values() { var me = this; if (!this.discount_amount_applied) { - for (const item of this.frm.doc._items || []) { + for (const item of this.frm._items || []) { frappe.model.round_floats_in(item); item.net_rate = item.rate; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; @@ -209,7 +209,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }); if(has_inclusive_tax==false) return; - $.each(me.frm.doc._items || [], function(n, item) { + $.each(me.frm._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var cumulated_tax_fraction = 0.0; var total_inclusive_tax_amount_per_qty = 0; @@ -280,13 +280,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var me = this; this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0; - $.each(this.frm.doc._items || [], function(i, item) { + $.each(this.frm._items || [], function(i, item) { me.frm.doc.total += item.amount; me.frm.doc.total_qty += item.qty; me.frm.doc.base_total += item.base_amount; me.frm.doc.net_total += item.net_amount; me.frm.doc.base_net_total += item.base_net_amount; - }); + }); } calculate_shipping_charges() { @@ -333,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } }); - $.each(this.frm.doc._items || [], function(n, item) { + $.each(this.frm._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); $.each(me.frm.doc["taxes"] || [], function(i, tax) { // tax_amount represents the amount of tax for the current step @@ -342,7 +342,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Adjust divisional loss to the last item if (tax.charge_type == "Actual") { actual_tax_dict[tax.idx] -= current_tax_amount; - if (n == me.frm.doc._items.length - 1) { + if (n == me.frm._items.length - 1) { current_tax_amount += actual_tax_dict[tax.idx]; } } @@ -379,7 +379,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } // set precision in the last item iteration - if (n == me.frm.doc._items.length - 1) { + if (n == me.frm._items.length - 1) { me.round_off_totals(tax); me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]); @@ -602,7 +602,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { _cleanup() { this.frm.doc.base_in_words = this.frm.doc.in_words = ""; - let items = this.frm.doc._items; + let items = this.frm._items; if(items && items.length) { if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) { @@ -659,7 +659,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var net_total = 0; // calculate item amount after Discount Amount if (total_for_discount_amount) { - $.each(this.frm.doc._items || [], function(i, item) { + $.each(this.frm._items || [], function(i, item) { distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount; item.net_amount = flt(item.net_amount - distributed_amount, precision("base_amount", item)); @@ -667,7 +667,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // discount amount rounding loss adjustment if no taxes if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total")) - && i == (me.frm.doc._items || []).length - 1) { + && i == (me.frm._items || []).length - 1) { var discount_amount_loss = flt(me.frm.doc.net_total - net_total - me.frm.doc.discount_amount, precision("net_total")); item.net_amount = flt(item.net_amount + discount_amount_loss, From 8133be4868079d410bb57ebbc5507a68c696b750 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 9 May 2023 16:30:09 +0530 Subject: [PATCH 21/22] fix: non manufacturing items/fixed asset items in BOM (cherry picked from commit aba8431d709a7ec7b92bf9df1460821955779cf3) --- erpnext/manufacturing/doctype/bom/bom.js | 4 +- erpnext/manufacturing/doctype/bom/bom.py | 5 ++- erpnext/manufacturing/doctype/bom/test_bom.py | 39 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 7cdcef9c7ae..066a373da24 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -48,7 +48,9 @@ frappe.ui.form.on("BOM", { return { query: "erpnext.manufacturing.doctype.bom.bom.item_query", filters: { - "item_code": doc.item + "item_code": doc.item, + "include_item_in_manufacturing": 1, + "is_fixed_asset": 0 } }; }); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b53149affd3..8058a5f8b75 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1339,8 +1339,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if not has_variants: query_filters["has_variants"] = 0 - if filters and filters.get("is_stock_item"): - query_filters["is_stock_item"] = 1 + if filters: + for fieldname, value in filters.items(): + query_filters[fieldname] = value return frappe.get_list( "Item", diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 01bf2e4315f..051b475bcc3 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -698,6 +698,45 @@ class TestBOM(FrappeTestCase): bom.update_cost() self.assertFalse(bom.flags.cost_updated) + def test_do_not_include_manufacturing_and_fixed_items(self): + from erpnext.manufacturing.doctype.bom.bom import item_query + + if not frappe.db.exists("Asset Category", "Computers-Test"): + doc = frappe.get_doc({"doctype": "Asset Category", "asset_category_name": "Computers-Test"}) + doc.flags.ignore_mandatory = True + doc.insert() + + for item_code, properties in { + "_Test RM Item 1 Do Not Include In Manufacture": { + "is_stock_item": 1, + "include_item_in_manufacturing": 0, + }, + "_Test RM Item 2 Fixed Asset Item": { + "is_fixed_asset": 1, + "is_stock_item": 0, + "asset_category": "Computers-Test", + }, + "_Test RM Item 3 Manufacture Item": {"is_stock_item": 1, "include_item_in_manufacturing": 1}, + }.items(): + make_item(item_code, properties) + + data = item_query( + "Item", + txt="_Test RM Item", + searchfield="name", + start=0, + page_len=20000, + filters={"include_item_in_manufacturing": 1, "is_fixed_asset": 0}, + ) + + items = [] + for row in data: + items.append(row[0]) + + self.assertTrue("_Test RM Item 1 Do Not Include In Manufacture" not in items) + self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items) + self.assertTrue("_Test RM Item 3 Manufacture Item" in items) + 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 709f94c8d3dc08b58b7e1a05c0a4b34f2f2b1a0d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 9 May 2023 18:45:19 +0530 Subject: [PATCH 22/22] fix: Changed type of column 'serial_no' in Stock Reconciliation to fix Data too long error (cherry picked from commit 1a673fd42451a855e9e9e1234a1d271ff17a18b4) --- .../stock_reconciliation_item.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index 7c3e151663b..2f65eaa358d 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -99,7 +99,7 @@ }, { "fieldname": "serial_no", - "fieldtype": "Small Text", + "fieldtype": "Long Text", "label": "Serial No" }, { @@ -120,7 +120,7 @@ }, { "fieldname": "current_serial_no", - "fieldtype": "Small Text", + "fieldtype": "Long Text", "label": "Current Serial No", "no_copy": 1, "print_hide": 1, @@ -189,7 +189,7 @@ ], "istable": 1, "links": [], - "modified": "2022-11-02 13:01:23.580937", + "modified": "2023-05-09 18:42:19.224916", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item",