From 14d8e621bb71dc5196a38200785db223c54a0ab3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 3 Jan 2023 19:05:41 +0530 Subject: [PATCH 01/19] refactor: revamp process loss feature & added tab breaks (cherry picked from commit ae039777f90c125a83b8f02e5b1c24b3f3ca3ef3) --- erpnext/manufacturing/doctype/bom/bom.js | 52 +++++---- erpnext/manufacturing/doctype/bom/bom.json | 109 +++++++++++++----- erpnext/manufacturing/doctype/bom/bom.py | 36 ++---- erpnext/manufacturing/doctype/bom/test_bom.py | 7 +- .../bom_scrap_item/bom_scrap_item.json | 10 +- .../doctype/work_order/test_work_order.py | 2 +- .../doctype/work_order/work_order.json | 43 +++++-- .../doctype/work_order/work_order.py | 48 ++++---- .../doctype/stock_entry/stock_entry.json | 109 +++++++++++++----- .../stock/doctype/stock_entry/stock_entry.py | 68 +++++------ .../stock_entry_detail.json | 9 +- 11 files changed, 283 insertions(+), 210 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index ecad41fe7b8..4dd8205a70c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -4,7 +4,7 @@ frappe.provide("erpnext.bom"); frappe.ui.form.on("BOM", { - setup: function(frm) { + setup(frm) { frm.custom_make_buttons = { 'Work Order': 'Work Order', 'Quality Inspection': 'Quality Inspection' @@ -65,11 +65,11 @@ frappe.ui.form.on("BOM", { }); }, - onload_post_render: function(frm) { + onload_post_render(frm) { frm.get_field("items").grid.set_multiple_add("item_code", "qty"); }, - refresh: function(frm) { + refresh(frm) { frm.toggle_enable("item", frm.doc.__islocal); frm.set_indicator_formatter('item_code', @@ -152,7 +152,7 @@ frappe.ui.form.on("BOM", { } }, - make_work_order: function(frm) { + make_work_order(frm) { frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", @@ -164,7 +164,7 @@ frappe.ui.form.on("BOM", { variant_items: variant_items }, freeze: true, - callback: function(r) { + callback(r) { if(r.message) { let doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); @@ -174,7 +174,7 @@ frappe.ui.form.on("BOM", { }); }, - make_variant_bom: function(frm) { + make_variant_bom(frm) { frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => { frappe.call({ method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", @@ -185,7 +185,7 @@ frappe.ui.form.on("BOM", { variant_items: variant_items }, freeze: true, - callback: function(r) { + callback(r) { if(r.message) { let doc = frappe.model.sync(r.message)[0]; frappe.set_route("Form", doc.doctype, doc.name); @@ -195,7 +195,7 @@ frappe.ui.form.on("BOM", { }, true); }, - setup_variant_prompt: function(frm, title, callback, skip_qty_field) { + setup_variant_prompt(frm, title, callback, skip_qty_field) { const fields = []; if (frm.doc.has_variants) { @@ -205,7 +205,7 @@ frappe.ui.form.on("BOM", { fieldname: 'item', options: "Item", reqd: 1, - get_query: function() { + get_query() { return { query: "erpnext.controllers.queries.item_query", filters: { @@ -273,7 +273,7 @@ frappe.ui.form.on("BOM", { fieldtype: "Link", in_list_view: 1, reqd: 1, - get_query: function(data) { + get_query(data) { if (!data.item_code) { frappe.throw(__("Select template item")); } @@ -308,7 +308,7 @@ frappe.ui.form.on("BOM", { ], in_place_edit: true, data: [], - get_data: function () { + get_data () { return []; }, }); @@ -343,14 +343,14 @@ frappe.ui.form.on("BOM", { } }, - make_quality_inspection: function(frm) { + make_quality_inspection(frm) { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection", frm: frm }) }, - update_cost: function(frm, save_doc=false) { + update_cost(frm, save_doc=false) { return frappe.call({ doc: frm.doc, method: "update_cost", @@ -360,26 +360,26 @@ frappe.ui.form.on("BOM", { save: save_doc, from_child_bom: false }, - callback: function(r) { + callback(r) { refresh_field("items"); if(!r.exc) frm.refresh_fields(); } }); }, - rm_cost_as_per: function(frm) { + rm_cost_as_per(frm) { if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { frm.set_value("plc_conversion_rate", 1.0); } }, - routing: function(frm) { + routing(frm) { if (frm.doc.routing) { frappe.call({ doc: frm.doc, method: "get_routing", freeze: true, - callback: function(r) { + callback(r) { if (!r.exc) { frm.refresh_fields(); erpnext.bom.calculate_op_cost(frm.doc); @@ -388,6 +388,16 @@ frappe.ui.form.on("BOM", { } }); } + }, + + process_loss_percentage(frm) { + let qty = 0.0 + if (frm.doc.process_loss_percentage) { + qty = (frm.doc.quantity * frm.doc.process_loss_percentage) / 100; + } + + frm.set_value("process_loss_qty", qty); + frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0); } }); @@ -479,10 +489,6 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { }, callback: function(r) { d = locals[cdt][cdn]; - if (d.is_process_loss) { - r.message.rate = 0; - r.message.base_rate = 0; - } $.extend(d, r.message); refresh_field("items"); @@ -717,10 +723,6 @@ frappe.tour['BOM'] = [ frappe.ui.form.on("BOM Scrap Item", { item_code(frm, cdt, cdn) { const { item_code } = locals[cdt][cdn]; - if (item_code === frm.doc.item) { - locals[cdt][cdn].is_process_loss = 1; - trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code); - } }, }); diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 0b441969400..c31b69f3dc5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -6,6 +6,7 @@ "document_type": "Setup", "engine": "InnoDB", "field_order": [ + "production_item_tab", "item", "company", "item_name", @@ -19,14 +20,15 @@ "quantity", "image", "currency_detail", - "currency", - "conversion_rate", - "column_break_12", "rm_cost_as_per", "buying_price_list", "price_list_currency", "plc_conversion_rate", + "column_break_ivyw", + "currency", + "conversion_rate", "section_break_21", + "operations_section_section", "with_operations", "column_break_23", "transfer_material_against", @@ -34,13 +36,14 @@ "operations_section", "operations", "materials_section", - "inspection_required", - "quality_inspection_template", - "column_break_31", - "section_break_33", "items", "scrap_section", + "scrap_items_section", "scrap_items", + "process_loss_section", + "process_loss_percentage", + "column_break_ssj2", + "process_loss_qty", "costing", "operating_cost", "raw_material_cost", @@ -52,10 +55,14 @@ "column_break_26", "total_cost", "base_total_cost", - "section_break_25", + "more_info_tab", "description", "column_break_27", "has_variants", + "quality_inspection_section_break", + "inspection_required", + "column_break_dxp7", + "quality_inspection_template", "section_break0", "exploded_items", "website_section", @@ -68,7 +75,8 @@ "show_items", "show_operations", "web_long_description", - "amended_from" + "amended_from", + "connections_tab" ], "fields": [ { @@ -183,7 +191,7 @@ { "fieldname": "currency_detail", "fieldtype": "Section Break", - "label": "Currency and Price List" + "label": "Cost Configuration" }, { "fieldname": "company", @@ -208,10 +216,6 @@ "precision": "9", "reqd": 1 }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - }, { "fieldname": "currency", "fieldtype": "Link", @@ -261,7 +265,7 @@ { "fieldname": "materials_section", "fieldtype": "Section Break", - "label": "Materials", + "label": "Raw Materials", "oldfieldtype": "Section Break" }, { @@ -276,18 +280,18 @@ { "collapsible": 1, "fieldname": "scrap_section", - "fieldtype": "Section Break", - "label": "Scrap" + "fieldtype": "Tab Break", + "label": "Scrap & Process Loss" }, { "fieldname": "scrap_items", "fieldtype": "Table", - "label": "Scrap Items", + "label": "Items", "options": "BOM Scrap Item" }, { "fieldname": "costing", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Costing", "oldfieldtype": "Section Break" }, @@ -379,10 +383,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "section_break_25", - "fieldtype": "Section Break" - }, { "fetch_from": "item.description", "fieldname": "description", @@ -478,8 +478,8 @@ }, { "fieldname": "section_break_21", - "fieldtype": "Section Break", - "label": "Operations" + "fieldtype": "Tab Break", + "label": "Operations & Materials" }, { "fieldname": "column_break_23", @@ -511,6 +511,7 @@ "fetch_from": "item.has_variants", "fieldname": "has_variants", "fieldtype": "Check", + "hidden": 1, "in_list_view": 1, "label": "Has Variants", "no_copy": 1, @@ -518,13 +519,63 @@ "read_only": 1 }, { - "fieldname": "column_break_31", + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "operations_section_section", + "fieldtype": "Section Break", + "label": "Operations" + }, + { + "fieldname": "process_loss_section", + "fieldtype": "Section Break", + "label": "Process Loss" + }, + { + "fieldname": "process_loss_percentage", + "fieldtype": "Percent", + "label": "% Process Loss" + }, + { + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "read_only": 1 + }, + { + "fieldname": "column_break_ssj2", "fieldtype": "Column Break" }, { - "fieldname": "section_break_33", + "fieldname": "more_info_tab", + "fieldtype": "Tab Break", + "label": "More Info" + }, + { + "fieldname": "column_break_dxp7", + "fieldtype": "Column Break" + }, + { + "fieldname": "quality_inspection_section_break", "fieldtype": "Section Break", - "hide_border": 1 + "label": "Quality Inspection" + }, + { + "fieldname": "production_item_tab", + "fieldtype": "Tab Break", + "label": "Production Item" + }, + { + "fieldname": "column_break_ivyw", + "fieldtype": "Column Break" + }, + { + "fieldname": "scrap_items_section", + "fieldtype": "Section Break", + "label": "Scrap Items" } ], "icon": "fa fa-sitemap", @@ -532,7 +583,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2022-01-30 21:27:54.727298", + "modified": "2023-01-03 18:42:27.732107", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index ca4f63df772..31f73963c84 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -233,6 +233,7 @@ class BOM(WebsiteGenerator): "sequence_id", "operation", "workstation", + "workstation_type", "description", "time_in_mins", "batch_size", @@ -877,35 +878,14 @@ class BOM(WebsiteGenerator): return BOMTree(self.name) def validate_scrap_items(self): - for item in self.scrap_items: - msg = "" - if item.item_code == self.item and not item.is_process_loss: - msg = _( - "Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked." - ).format(frappe.bold(item.item_code)) - elif item.item_code != self.item and item.is_process_loss: - msg = _( - "Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked" - ).format(frappe.bold(item.item_code)) + must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number") - must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") - if item.is_process_loss and must_be_whole_number: - msg = _( - "Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM." - ).format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) + if self.process_loss_percentage and self.process_loss_percentage > 100: + frappe.throw(_("Process Loss Percentage cannot be greater than 100")) - if item.is_process_loss and (item.stock_qty >= self.quantity): - msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format( - frappe.bold(item.item_code) - ) - - if item.is_process_loss and (item.rate > 0): - msg = _( - "Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked." - ).format(frappe.bold(item.item_code)) - - if msg: - frappe.throw(msg, title=_("Note")) + if self.process_loss_qty and must_be_whole_number and self.process_loss_qty % 1 != 0: + msg = f"Item: {frappe.bold(self.item)} with Stock UOM: {frappe.bold(self.uom)} can't have fractional process loss qty as UOM {frappe.bold(self.uom)} is a whole Number." + frappe.throw(msg, title=_("Invalid Process Loss Configuration")) def get_bom_item_rate(args, bom_doc): @@ -1053,7 +1033,7 @@ def get_bom_items_as_dict( query = query.format( table="BOM Scrap Item", where_conditions="", - select_columns=", item.description, is_process_loss", + select_columns=", item.description", is_stock_item=is_stock_item, qty_field="stock_qty", ) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index e34ac12cd23..989861717e4 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -409,7 +409,7 @@ class TestBOM(FrappeTestCase): self.assertRaises(frappe.ValidationError, bom_doc.submit) bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0 + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0 ) # FG Items in Scrap/Loss Table should have Is Process Loss set self.assertRaises(frappe.ValidationError, bom_doc.submit) @@ -743,9 +743,7 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) -def create_bom_with_process_loss_item( - fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1 -): +def create_bom_with_process_loss_item(fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2): bom_doc = frappe.new_doc("BOM") bom_doc.item = fg_item.item_code bom_doc.quantity = fg_qty @@ -768,7 +766,6 @@ def create_bom_with_process_loss_item( "uom": fg_item.stock_uom, "stock_uom": fg_item.stock_uom, "rate": scrap_rate, - "is_process_loss": is_process_loss, }, ) bom_doc.currency = "INR" diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json index 7018082e402..b2ef19b20f0 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json @@ -8,7 +8,6 @@ "item_code", "column_break_2", "item_name", - "is_process_loss", "quantity_and_rate", "stock_qty", "rate", @@ -89,17 +88,11 @@ { "fieldname": "column_break_2", "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "is_process_loss", - "fieldtype": "Check", - "label": "Is Process Loss" } ], "istable": 1, "links": [], - "modified": "2021-06-22 16:46:12.153311", + "modified": "2023-01-03 14:19:28.460965", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Scrap Item", @@ -108,5 +101,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f568264c908..6c7483ca7d2 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -859,7 +859,7 @@ class TestWorkOrder(FrappeTestCase): bom_no = f"BOM-{fg_item_non_whole.item_code}-001" if not frappe.db.exists("BOM", bom_no): bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1, is_process_loss=1 + fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1 ) bom_doc.submit() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 9452a63d70b..25e16d63376 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -14,13 +14,13 @@ "item_name", "image", "bom_no", + "sales_order", "column_break1", "company", "qty", "material_transferred_for_manufacturing", "produced_qty", "process_loss_qty", - "sales_order", "project", "serial_no_and_batch_for_finished_good_section", "has_serial_no", @@ -28,6 +28,7 @@ "column_break_17", "serial_no", "batch_size", + "work_order_configuration", "settings_section", "allow_alternative_item", "use_multi_level_bom", @@ -42,7 +43,11 @@ "fg_warehouse", "scrap_warehouse", "required_items_section", + "materials_and_operations_tab", "required_items", + "operations_section", + "operations", + "transfer_material_against", "time", "planned_start_date", "planned_end_date", @@ -51,9 +56,6 @@ "actual_start_date", "actual_end_date", "lead_time", - "operations_section", - "transfer_material_against", - "operations", "section_break_22", "planned_operating_cost", "actual_operating_cost", @@ -72,12 +74,14 @@ "production_plan_item", "production_plan_sub_assembly_item", "product_bundle_item", - "amended_from" + "amended_from", + "connections_tab" ], "fields": [ { "fieldname": "item", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", + "label": "Production Item", "options": "fa fa-gift" }, { @@ -236,7 +240,7 @@ { "fieldname": "warehouses", "fieldtype": "Section Break", - "label": "Warehouses", + "label": "Warehouse", "options": "fa fa-building" }, { @@ -390,8 +394,8 @@ { "collapsible": 1, "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Information", + "fieldtype": "Tab Break", + "label": "More Info", "options": "fa fa-file-text" }, { @@ -474,8 +478,7 @@ }, { "fieldname": "settings_section", - "fieldtype": "Section Break", - "label": "Settings" + "fieldtype": "Section Break" }, { "fieldname": "column_break_18", @@ -568,6 +571,22 @@ "no_copy": 1, "non_negative": 1, "read_only": 1 + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "work_order_configuration", + "fieldtype": "Tab Break", + "label": "Configuration" + }, + { + "fieldname": "materials_and_operations_tab", + "fieldtype": "Tab Break", + "label": "Materials & Operations" } ], "icon": "fa fa-cogs", @@ -575,7 +594,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2022-01-24 21:18:12.160114", + "modified": "2023-01-03 14:16:35.427731", "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 52753a092d4..2b30641ff3f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -285,14 +285,7 @@ class WorkOrder(Document): ): continue - qty = flt( - frappe.db.sql( - """select sum(fg_completed_qty) - from `tabStock Entry` where work_order=%s and docstatus=1 - and purpose=%s""", - (self.name, purpose), - )[0][0] - ) + qty = self.get_transferred_or_manufactured_qty(purpose) completed_qty = self.qty + (allowance_percentage / 100 * self.qty) if qty > completed_qty: @@ -314,26 +307,27 @@ class WorkOrder(Document): if self.production_plan: self.update_production_plan_status() - def set_process_loss_qty(self): - process_loss_qty = flt( - frappe.db.sql( - """ - SELECT sum(qty) FROM `tabStock Entry Detail` - WHERE - is_process_loss=1 - AND parent IN ( - SELECT name FROM `tabStock Entry` - WHERE - work_order=%s - AND purpose='Manufacture' - AND docstatus=1 - ) - """, - (self.name,), - )[0][0] + def get_transferred_or_manufactured_qty(self, purpose): + table = frappe.qb.DocType("Stock Entry") + query = ( + frappe.qb.from_(table) + .select(Sum(table.fg_completed_qty)) + .where((table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose)) ) - if process_loss_qty is not None: - self.db_set("process_loss_qty", process_loss_qty) + + return flt(query.run()[0][0]) + + def set_process_loss_qty(self): + table = frappe.qb.DocType("Stock Entry") + process_loss_qty = ( + frappe.qb.from_(table) + .select(Sum(table.process_loss_qty)) + .where( + (table.work_order == self.name) & (table.purpose == "Manufacture") & (table.docstatus == 1) + ) + ).run()[0][0] + + self.db_set("process_loss_qty", flt(process_loss_qty)) def update_production_plan_status(self): production_plan = frappe.get_doc("Production Plan", self.production_plan) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 7e9420d5035..9c0f1fc03f4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -7,7 +7,7 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ - "items_section", + "stock_entry_details_tab", "naming_series", "stock_entry_type", "outgoing_stock_entry", @@ -26,15 +26,20 @@ "posting_time", "set_posting_time", "inspection_required", - "from_bom", "apply_putaway_rule", - "sb1", - "bom_no", - "fg_completed_qty", - "cb1", + "items_tab", + "bom_info_section", + "from_bom", "use_multi_level_bom", + "bom_no", + "cb1", + "fg_completed_qty", "get_items", - "section_break_12", + "section_break_7qsm", + "process_loss_percentage", + "column_break_e92r", + "process_loss_qty", + "section_break_jwgn", "from_warehouse", "source_warehouse_address", "source_address_display", @@ -44,6 +49,7 @@ "target_address_display", "sb0", "scan_barcode", + "items_section", "items", "get_stock_and_rate", "section_break_19", @@ -54,6 +60,7 @@ "additional_costs_section", "additional_costs", "total_additional_costs", + "supplier_info_tab", "contact_section", "supplier", "supplier_name", @@ -61,7 +68,7 @@ "address_display", "accounting_dimensions_section", "project", - "dimension_col_break", + "other_info_tab", "printing_settings", "select_print_heading", "print_settings_col_break", @@ -78,11 +85,6 @@ "is_return" ], "fields": [ - { - "fieldname": "items_section", - "fieldtype": "Section Break", - "oldfieldtype": "Section Break" - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -236,17 +238,12 @@ }, { "default": "0", - "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \t\t\t\t\t\"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", + "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", "fieldname": "from_bom", "fieldtype": "Check", "label": "From BOM", "print_hide": 1 }, - { - "depends_on": "eval: doc.from_bom && (doc.purpose!==\"Sales Return\" && doc.purpose!==\"Purchase Return\")", - "fieldname": "sb1", - "fieldtype": "Section Break" - }, { "depends_on": "from_bom", "fieldname": "bom_no", @@ -285,10 +282,6 @@ "oldfieldtype": "Button", "print_hide": 1 }, - { - "fieldname": "section_break_12", - "fieldtype": "Section Break" - }, { "description": "Sets 'Source Warehouse' in each row of the items table.", "fieldname": "from_warehouse", @@ -411,7 +404,7 @@ "collapsible": 1, "collapsible_depends_on": "total_additional_costs", "fieldname": "additional_costs_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Additional Costs" }, { @@ -576,13 +569,9 @@ { "collapsible": 1, "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Accounting Dimensions" }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - }, { "fieldname": "pick_list", "fieldtype": "Link", @@ -621,6 +610,66 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "items_tab", + "fieldtype": "Tab Break", + "label": "Items" + }, + { + "fieldname": "bom_info_section", + "fieldtype": "Section Break", + "label": "BOM Info" + }, + { + "collapsible": 1, + "fieldname": "section_break_jwgn", + "fieldtype": "Section Break", + "label": "Default Warehouse" + }, + { + "fieldname": "other_info_tab", + "fieldtype": "Tab Break", + "label": "Other Info" + }, + { + "fieldname": "supplier_info_tab", + "fieldtype": "Tab Break", + "label": "Supplier Info" + }, + { + "fieldname": "stock_entry_details_tab", + "fieldtype": "Tab Break", + "label": "Details", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "section_break_7qsm", + "fieldtype": "Section Break" + }, + { + "depends_on": "process_loss_percentage", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "read_only": 1 + }, + { + "fieldname": "column_break_e92r", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.from_bom && doc.fg_completed_qty", + "fetch_from": "bom_no.process_loss_percentage", + "fetch_if_empty": 1, + "fieldname": "process_loss_percentage", + "fieldtype": "Percent", + "label": "% Process Loss" + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "label": "Items" } ], "icon": "fa fa-file-text", @@ -628,7 +677,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-10-07 14:39:51.943770", + "modified": "2023-01-03 16:02:50.741816", "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 fc3a50ededb..0342210eb96 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -116,6 +116,7 @@ class StockEntry(StockController): self.validate_warehouse() self.validate_work_order() self.validate_bom() + self.set_process_loss_qty() self.validate_purchase_order() self.validate_subcontracting_order() @@ -126,7 +127,7 @@ class StockEntry(StockController): self.validate_with_material_request() self.validate_batch() self.validate_inspection() - # self.validate_fg_completed_qty() + self.validate_fg_completed_qty() self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() @@ -388,11 +389,20 @@ class StockEntry(StockController): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item or d.is_process_loss: + if d.is_finished_item: item_wise_qty.setdefault(d.item_code, []).append(d.qty) + precision = frappe.get_precision("Stock Entry Detail", "qty") for item_code, qty_list in item_wise_qty.items(): - total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) + total = flt(sum(qty_list), precision) + + if (self.fg_completed_qty - total) > 0: + self.process_loss_qty = flt(self.fg_completed_qty - total, precision) + self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) + + if self.process_loss_qty: + total += flt(self.process_loss_qty, precision) + if self.fg_completed_qty != total: frappe.throw( _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( @@ -471,7 +481,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.is_finished_item or d.is_scrap_item or d.is_process_loss: + if d.is_finished_item or d.is_scrap_item: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -648,9 +658,7 @@ class StockEntry(StockController): 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 or d.is_process_loss - ) + 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"): @@ -689,8 +697,6 @@ class StockEntry(StockController): # do not round off basic rate to avoid precision loss d.basic_rate = flt(d.basic_rate) - if d.is_process_loss: - d.basic_rate = flt(0.0) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): @@ -1469,11 +1475,11 @@ class StockEntry(StockController): # add finished goods item if self.purpose in ("Manufacture", "Repack"): + self.set_process_loss_qty() self.load_items_from_bom() self.set_scrap_items() self.set_actual_qty() - self.update_items_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount(raise_error_if_no_rate=False) @@ -1486,6 +1492,20 @@ class StockEntry(StockController): self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) + def set_process_loss_qty(self): + if self.purpose not in ("Manufacture", "Repack"): + return + + self.process_loss_qty = 0.0 + self.process_loss_percentage = frappe.get_cached_value( + "BOM", self.bom_no, "process_loss_percentage" + ) + + if self.process_loss_percentage: + self.process_loss_qty = flt( + (flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100 + ) + def set_work_order_details(self): if not getattr(self, "pro_doc", None): self.pro_doc = frappe._dict() @@ -1518,7 +1538,7 @@ class StockEntry(StockController): args = { "to_warehouse": to_warehouse, "from_warehouse": "", - "qty": self.fg_completed_qty, + "qty": flt(self.fg_completed_qty) - flt(self.process_loss_qty), "item_name": item.item_name, "description": item.description, "stock_uom": item.stock_uom, @@ -1966,7 +1986,6 @@ class StockEntry(StockController): ) se_child.is_finished_item = item_row.get("is_finished_item", 0) se_child.is_scrap_item = item_row.get("is_scrap_item", 0) - se_child.is_process_loss = item_row.get("is_process_loss", 0) se_child.po_detail = item_row.get("po_detail") se_child.sco_rm_detail = item_row.get("sco_rm_detail") @@ -2213,31 +2232,6 @@ class StockEntry(StockController): material_requests.append(material_request) frappe.db.set_value("Material Request", material_request, "transfer_status", status) - def update_items_for_process_loss(self): - process_loss_dict = {} - for d in self.get("items"): - if not d.is_process_loss: - continue - - scrap_warehouse = frappe.db.get_single_value( - "Manufacturing Settings", "default_scrap_warehouse" - ) - if scrap_warehouse is not None: - d.t_warehouse = scrap_warehouse - d.is_scrap_item = 0 - - if d.item_code not in process_loss_dict: - process_loss_dict[d.item_code] = [flt(0), flt(0)] - process_loss_dict[d.item_code][0] += flt(d.transfer_qty) - process_loss_dict[d.item_code][1] += flt(d.qty) - - for d in self.get("items"): - if not d.is_finished_item or d.item_code not in process_loss_dict: - continue - # Assumption: 1 finished item has 1 row. - d.transfer_qty -= process_loss_dict[d.item_code][0] - d.qty -= process_loss_dict[d.item_code][1] - def set_serial_no_batch_for_finished_good(self): serial_nos = [] if self.pro_doc.serial_no: 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 95f4f5fd369..fe81a87558c 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -20,7 +20,6 @@ "is_finished_item", "is_scrap_item", "quality_inspection", - "is_process_loss", "subcontracted_item", "section_break_8", "description", @@ -559,12 +558,6 @@ "print_hide": 1, "read_only": 1 }, - { - "default": "0", - "fieldname": "is_process_loss", - "fieldtype": "Check", - "label": "Is Process Loss" - }, { "default": "0", "depends_on": "barcode", @@ -578,7 +571,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-02 13:00:34.258828", + "modified": "2023-01-03 14:51:16.575515", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 28d5990326ee28e5ea2c7ef4a9e27dba003f867b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 3 Jan 2023 22:47:52 +0530 Subject: [PATCH 02/19] test: test cases for process loss (cherry picked from commit 524c0994e05c67ec00bb91e81abe725d6f77899a) --- erpnext/manufacturing/doctype/bom/bom.py | 5 ++ erpnext/manufacturing/doctype/bom/test_bom.py | 56 +++++++------------ .../doctype/work_order/test_work_order.py | 23 +++----- 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 31f73963c84..53af28df8a5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -193,6 +193,7 @@ class BOM(WebsiteGenerator): self.update_exploded_items(save=False) self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) + self.set_process_loss_qty() self.validate_scrap_items() def get_context(self, context): @@ -877,6 +878,10 @@ class BOM(WebsiteGenerator): """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) + def set_process_loss_qty(self): + if self.process_loss_percentage: + self.process_loss_qty = flt(self.quantity) * flt(self.process_loss_percentage) / 100 + def validate_scrap_items(self): must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number") diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 989861717e4..16f5c793720 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -384,36 +384,16 @@ class TestBOM(FrappeTestCase): def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() - if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"): - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1 - ) - bom_doc.submit() - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 + fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0, process_loss_percentage=110 ) - # PL Item qty can't be >= FG Item qty + # PL can't be > 100 self.assertRaises(frappe.ValidationError, bom_doc.submit) - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100 - ) - # PL Item rate has to be 0 - self.assertRaises(frappe.ValidationError, bom_doc.submit) - - bom_doc = create_bom_with_process_loss_item( - fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0 - ) + bom_doc = create_bom_with_process_loss_item(fg_item_whole, bom_item, process_loss_percentage=20) # Items with whole UOMs can't be PL Items self.assertRaises(frappe.ValidationError, bom_doc.submit) - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0 - ) - # FG Items in Scrap/Loss Table should have Is Process Loss set - self.assertRaises(frappe.ValidationError, bom_doc.submit) - def test_bom_item_query(self): query = partial( item_query, @@ -743,7 +723,9 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) -def create_bom_with_process_loss_item(fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2): +def create_bom_with_process_loss_item( + fg_item, bom_item, scrap_qty=0, scrap_rate=0, fg_qty=2, process_loss_percentage=0 +): bom_doc = frappe.new_doc("BOM") bom_doc.item = fg_item.item_code bom_doc.quantity = fg_qty @@ -757,18 +739,22 @@ def create_bom_with_process_loss_item(fg_item, bom_item, scrap_qty, scrap_rate, "rate": 100.0, }, ) - bom_doc.append( - "scrap_items", - { - "item_code": fg_item.item_code, - "qty": scrap_qty, - "stock_qty": scrap_qty, - "uom": fg_item.stock_uom, - "stock_uom": fg_item.stock_uom, - "rate": scrap_rate, - }, - ) + + if scrap_qty: + bom_doc.append( + "scrap_items", + { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + }, + ) + bom_doc.currency = "INR" + bom_doc.process_loss_percentage = process_loss_percentage return bom_doc diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 6c7483ca7d2..76040b29d59 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -846,20 +846,20 @@ class TestWorkOrder(FrappeTestCase): create_process_loss_bom_items, ) - qty = 4 + qty = 10 scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" fg_item_non_whole, _, bom_item = create_process_loss_bom_items() test_stock_entry.make_stock_entry( - item_code=bom_item.item_code, target=source_warehouse, qty=4, basic_rate=100 + item_code=bom_item.item_code, target=source_warehouse, qty=qty, basic_rate=100 ) bom_no = f"BOM-{fg_item_non_whole.item_code}-001" if not frappe.db.exists("BOM", bom_no): bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1 + fg_item_non_whole, bom_item, fg_qty=1, process_loss_percentage=10 ) bom_doc.submit() @@ -883,19 +883,12 @@ class TestWorkOrder(FrappeTestCase): # Testing stock entry values items = se.get("items") - self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + self.assertEqual(len(items), 2, "There should be 3 items including process loss.") + fg_item = items[1] - source_item, fg_item, pl_item = items - - total_pl_qty = qty * scrap_qty - actual_fg_qty = qty - total_pl_qty - - self.assertEqual(pl_item.qty, total_pl_qty) - self.assertEqual(fg_item.qty, actual_fg_qty) - - # Testing Work Order values - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), total_pl_qty) + self.assertEqual(fg_item.qty, qty - 1) + self.assertEqual(se.process_loss_percentage, 10) + self.assertEqual(se.process_loss_qty, 1) @timeout(seconds=60) def test_job_card_scrap_item(self): From 3dab539719cc508cba1b64be177051a68c0f2a78 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 5 Jan 2023 12:25:50 +0530 Subject: [PATCH 03/19] chore: enable `No Copy` attribute for `route` in Item Group (cherry picked from commit 348dc3251485f0204430cdb8b42e27a5bcf07f9a) --- erpnext/setup/doctype/item_group/item_group.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 50f923d87e0..2986087277c 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -123,6 +123,7 @@ "fieldname": "route", "fieldtype": "Data", "label": "Route", + "no_copy": 1, "unique": 1 }, { @@ -232,11 +233,10 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2022-03-09 12:27:11.055782", + "modified": "2023-01-05 12:21:30.458628", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", - "name_case": "Title Case", "naming_rule": "By fieldname", "nsm_parent_field": "parent_item_group", "owner": "Administrator", From 031841d58e169ae8c6e179fd9818eb0467e3284d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Jan 2023 11:07:52 +0530 Subject: [PATCH 04/19] refactor: Sales Partner column in AR and AR Summary Report (cherry picked from commit ee94127974140eccf4393f5190b1626a551e5e2c) --- .../accounts_receivable/accounts_receivable.py | 12 ++++++++++-- .../accounts_receivable_summary.py | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index fb2e444abd1..94a1510f095 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -810,7 +810,7 @@ class ReceivablePayableReport(object): self.ple.party.isin( qb.from_(self.customer) .select(self.customer.name) - .where(self.customer.default_sales_partner == self.filters.get("payment_terms_template")) + .where(self.customer.default_sales_partner == self.filters.get("sales_partner")) ) ) @@ -869,10 +869,15 @@ class ReceivablePayableReport(object): def get_party_details(self, party): if not party in self.party_details: if self.party_type == "Customer": + fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"] + + if self.filters.get("sales_partner"): + fields.append("default_sales_partner") + self.party_details[party] = frappe.db.get_value( "Customer", party, - ["customer_name", "territory", "customer_group", "customer_primary_contact"], + fields, as_dict=True, ) else: @@ -973,6 +978,9 @@ class ReceivablePayableReport(object): if self.filters.show_sales_person: self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") + if self.filters.sales_partner: + self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") + if self.filters.party_type == "Supplier": self.add_column( label=_("Supplier Group"), diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 889f5a22a8a..29217b04be2 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -121,6 +121,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): if row.sales_person: self.party_total[row.party].sales_person.append(row.sales_person) + if self.filters.sales_partner: + self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner") + def get_columns(self): self.columns = [] self.add_column( @@ -160,6 +163,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) if self.filters.show_sales_person: self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") + + if self.filters.sales_partner: + self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") + else: self.add_column( label=_("Supplier Group"), From 2658fc9f9b022cac6806ef4aa1152c392c9afc0f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 5 Jan 2023 14:24:58 +0530 Subject: [PATCH 05/19] fix: incorrect status in the work order (cherry picked from commit b0baba84a0f821ad0cb515a01da2205bb6610b7f) --- .../doctype/work_order/test_work_order.py | 3 ++ .../doctype/work_order/work_order.py | 28 ++++++++----------- .../stock/doctype/stock_entry/stock_entry.py | 9 +++--- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 76040b29d59..729ed42f51a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -890,6 +890,9 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(se.process_loss_percentage, 10) self.assertEqual(se.process_loss_qty, 1) + wo.load_from_db() + self.assertEqual(wo.status, "In Process") + @timeout(seconds=60) def test_job_card_scrap_item(self): items = [ diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2b30641ff3f..ae9e9c69628 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -246,21 +246,11 @@ class WorkOrder(Document): status = "Draft" elif self.docstatus == 1: if status != "Stopped": - stock_entries = frappe._dict( - frappe.db.sql( - """select purpose, sum(fg_completed_qty) - from `tabStock Entry` where work_order=%s and docstatus=1 - group by purpose""", - self.name, - ) - ) - status = "Not Started" - if stock_entries: + if flt(self.material_transferred_for_manufacturing) > 0: status = "In Process" - produced_qty = stock_entries.get("Manufacture") - if flt(produced_qty) >= flt(self.qty): - status = "Completed" + if flt(self.produced_qty) >= flt(self.qty): + status = "Completed" else: status = "Cancelled" @@ -309,12 +299,15 @@ class WorkOrder(Document): def get_transferred_or_manufactured_qty(self, purpose): table = frappe.qb.DocType("Stock Entry") - query = ( - frappe.qb.from_(table) - .select(Sum(table.fg_completed_qty)) - .where((table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose)) + query = frappe.qb.from_(table).where( + (table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose) ) + if purpose == "Manufacture": + query = query.select(Sum(table.fg_completed_qty) - Sum(table.process_loss_qty)) + else: + query = query.select(Sum(table.fg_completed_qty)) + return flt(query.run()[0][0]) def set_process_loss_qty(self): @@ -346,6 +339,7 @@ class WorkOrder(Document): produced_qty = total_qty[0][0] if total_qty else 0 + self.update_status() production_plan.run_method( "update_produced_pending_qty", produced_qty, self.production_plan_item ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0342210eb96..a5264ac195a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1247,7 +1247,6 @@ class StockEntry(StockController): if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) _validate_work_order(pro_doc) - pro_doc.run_method("update_status") if self.fg_completed_qty: pro_doc.run_method("update_work_order_qty") @@ -1255,6 +1254,7 @@ class StockEntry(StockController): pro_doc.run_method("update_planned_qty") pro_doc.update_batch_produced_qty(self) + pro_doc.run_method("update_status") if not pro_doc.operations: pro_doc.set_actual_dates() @@ -1497,9 +1497,10 @@ class StockEntry(StockController): return self.process_loss_qty = 0.0 - self.process_loss_percentage = frappe.get_cached_value( - "BOM", self.bom_no, "process_loss_percentage" - ) + if not self.process_loss_percentage: + self.process_loss_percentage = frappe.get_cached_value( + "BOM", self.bom_no, "process_loss_percentage" + ) if self.process_loss_percentage: self.process_loss_qty = flt( From fe82ebcc38b1eed26a6b722aadfa304f895694b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 14:00:13 +0530 Subject: [PATCH 06/19] fix: Exchange gain and loss booking on multi-currency invoice reconciliation (#32900) fix: Exchange gain and loss booking on multi-currency invoice reconciliation (#32900) * fix: Exchange gain and loss booking on multi-curreny invoice reconciliation * test: Update test cases * chore: Ignore SQL linting rule * chore: Joural Entry for exchange gainand loss booking * chore: Journal entry for exchange gain loss booking * test: Update test case * chore: Default exchange gain and loss account (cherry picked from commit 9a3d947e893a787834bf12a9cf50c4af9e449f40) Co-authored-by: Deepesh Garg --- .../doctype/journal_entry/journal_entry.py | 18 +- .../payment_reconciliation.js | 16 +- .../payment_reconciliation.py | 132 ++++++++++-- .../test_payment_reconciliation.py | 198 +++++++++++++++--- .../payment_reconciliation_allocation.json | 26 ++- .../payment_reconciliation_invoice.json | 12 +- .../payment_reconciliation_payment.json | 14 +- 7 files changed, 349 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 5a1b6ba1712..88b030cae3d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -592,15 +592,15 @@ class JournalEntry(AccountsController): d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field) else: for d in self.get("accounts"): - if flt(d.debit > 0): + if flt(d.debit) > 0: accounts_debited.append(d.party or d.account) if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) for d in self.get("accounts"): - if flt(d.debit > 0): + if flt(d.debit) > 0: d.against_account = ", ".join(list(set(accounts_credited))) - if flt(d.credit > 0): + if flt(d.credit) > 0: d.against_account = ", ".join(list(set(accounts_debited))) def validate_debit_credit_amount(self): @@ -762,7 +762,7 @@ class JournalEntry(AccountsController): pay_to_recd_from = d.party if pay_to_recd_from and pay_to_recd_from == d.party: - party_amount += d.debit_in_account_currency or d.credit_in_account_currency + party_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency) party_account_currency = d.account_currency elif frappe.db.get_value("Account", d.account, "account_type") in ["Bank", "Cash"]: @@ -840,7 +840,7 @@ class JournalEntry(AccountsController): make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) @frappe.whitelist() - def get_balance(self): + def get_balance(self, difference_account=None): if not self.get("accounts"): msgprint(_("'Entries' cannot be empty"), raise_exception=True) else: @@ -855,7 +855,13 @@ class JournalEntry(AccountsController): blank_row = d if not blank_row: - blank_row = self.append("accounts", {}) + blank_row = self.append( + "accounts", + { + "account": difference_account, + "cost_center": erpnext.get_default_cost_center(self.company), + }, + ) blank_row.exchange_rate = 1 if diff > 0: diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 0b334ae076d..d986f320669 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -170,7 +170,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } reconcile() { - var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account); + var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount); if (show_dialog && show_dialog.length) { @@ -179,8 +179,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo title: __("Select Difference Account"), fields: [ { - fieldname: "allocation", fieldtype: "Table", label: __("Allocation"), - data: this.data, in_place_edit: true, + fieldname: "allocation", + fieldtype: "Table", + label: __("Allocation"), + data: this.data, + in_place_edit: true, + cannot_add_rows: true, get_data: () => { return this.data; }, @@ -218,6 +222,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo read_only: 1 }] }, + { + fieldtype: 'HTML', + options: " New Journal Entry will be posted for the difference amount " + } ], primary_action: () => { const args = dialog.get_values()["allocation"]; @@ -234,7 +242,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo }); this.frm.doc.allocation.forEach(d => { - if (d.difference_amount && !d.difference_account) { + if (d.difference_amount) { dialog.fields_dict.allocation.df.data.push({ 'docname': d.name, 'reference_name': d.reference_name, diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index ff212f2a35f..ac033f7db60 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -14,7 +14,6 @@ from erpnext.accounts.utils import ( QueryPaymentLedger, get_outstanding_invoices, reconcile_against_document, - update_reference_in_payment_entry, ) from erpnext.controllers.accounts_controller import get_advance_payment_entries @@ -80,12 +79,13 @@ class PaymentReconciliation(Document): "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" ) + # nosemgrep journal_entries = frappe.db.sql( """ select "Journal Entry" as reference_type, t1.name as reference_name, t1.posting_date, t1.remark as remarks, t2.name as reference_row, - {dr_or_cr} as amount, t2.is_advance, + {dr_or_cr} as amount, t2.is_advance, t2.exchange_rate, t2.account_currency as currency from `tabJournal Entry` t1, `tabJournal Entry Account` t2 @@ -215,26 +215,26 @@ class PaymentReconciliation(Document): inv.currency = entry.get("currency") inv.outstanding_amount = flt(entry.get("outstanding_amount")) - def get_difference_amount(self, allocated_entry): - if allocated_entry.get("reference_type") != "Payment Entry": - return + def get_difference_amount(self, payment_entry, invoice, allocated_amount): + difference_amount = 0 + if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( + "exchange_rate", 1 + ): + allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount + allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount + difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate - dr_or_cr = ( - "credit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == "Receivable" - else "debit_in_account_currency" - ) - - row = self.get_payment_details(allocated_entry, dr_or_cr) - - doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name) - update_reference_in_payment_entry(row, doc, do_not_save=True) - - return doc.difference_amount + return difference_amount @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() + + invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices")) + default_exchange_gain_loss_account = frappe.get_cached_value( + "Company", self.company, "exchange_gain_loss_account" + ) + entries = [] for pay in args.get("payments"): pay.update({"unreconciled_amount": pay.get("amount")}) @@ -248,7 +248,10 @@ class PaymentReconciliation(Document): inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount")) pay["amount"] = 0 - res.difference_amount = self.get_difference_amount(res) + inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number")) + res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) + res.difference_account = default_exchange_gain_loss_account + res.exchange_rate = inv.get("exchange_rate") if pay.get("amount") == 0: entries.append(res) @@ -278,6 +281,7 @@ class PaymentReconciliation(Document): "amount": pay.get("amount"), "allocated_amount": allocated_amount, "difference_amount": pay.get("difference_amount"), + "currency": inv.get("currency"), } ) @@ -300,7 +304,11 @@ class PaymentReconciliation(Document): else: reconciled_entry = entry_list - reconciled_entry.append(self.get_payment_details(row, dr_or_cr)) + payment_details = self.get_payment_details(row, dr_or_cr) + reconciled_entry.append(payment_details) + + if payment_details.difference_amount: + self.make_difference_entry(payment_details) if entry_list: reconcile_against_document(entry_list) @@ -311,6 +319,56 @@ class PaymentReconciliation(Document): msgprint(_("Successfully Reconciled")) self.get_unreconciled_entries() + def make_difference_entry(self, row): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account_currency = frappe.get_cached_value( + "Account", self.receivable_payable_account, "account_currency" + ) + difference_account_currency = frappe.get_cached_value( + "Account", row.difference_account, "account_currency" + ) + + # Account Currency has balance + dr_or_cr = "debit" if self.party_type == "Customer" else "debit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + journal_account = frappe._dict( + { + "account": self.receivable_payable_account, + "party_type": self.party_type, + "party": self.party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": row.against_voucher_type, + "reference_name": row.against_voucher, + dr_or_cr: flt(row.difference_amount), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": row.difference_account, + "account_currency": difference_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + def get_payment_details(self, row, dr_or_cr): return frappe._dict( { @@ -320,6 +378,7 @@ class PaymentReconciliation(Document): "against_voucher_type": row.get("invoice_type"), "against_voucher": row.get("invoice_number"), "account": self.receivable_payable_account, + "exchange_rate": row.get("exchange_rate"), "party_type": self.party_type, "party": self.party, "is_advance": row.get("is_advance"), @@ -344,6 +403,41 @@ class PaymentReconciliation(Document): if not self.get("payments"): frappe.throw(_("No records found in the Payments table")) + def get_invoice_exchange_map(self, invoices): + sales_invoices = [ + d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice" + ] + purchase_invoices = [ + d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice" + ] + invoice_exchange_map = frappe._dict() + + if sales_invoices: + sales_invoice_map = frappe._dict( + frappe.db.get_all( + "Sales Invoice", + filters={"name": ("in", sales_invoices)}, + fields=["name", "conversion_rate"], + as_list=1, + ) + ) + + invoice_exchange_map.update(sales_invoice_map) + + if purchase_invoices: + purchase_invoice_map = frappe._dict( + frappe.db.get_all( + "Purchase Invoice", + filters={"name": ("in", purchase_invoices)}, + fields=["name", "conversion_rate"], + as_list=1, + ) + ) + + invoice_exchange_map.update(purchase_invoice_map) + + return invoice_exchange_map + def validate_allocation(self): unreconciled_invoices = frappe._dict() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 6030134fff2..2ba90b4da9f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -6,7 +6,7 @@ import unittest import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, nowdate +from frappe.utils import add_days, flt, nowdate from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -75,33 +75,11 @@ class TestPaymentReconciliation(FrappeTestCase): self.item = item if isinstance(item, str) else item.item_code def create_customer(self): - if frappe.db.exists("Customer", "_Test PR Customer"): - self.customer = "_Test PR Customer" - else: - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test PR Customer" - customer.type = "Individual" - customer.save() - self.customer = customer.name - - if frappe.db.exists("Customer", "_Test PR Customer 2"): - self.customer2 = "_Test PR Customer 2" - else: - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test PR Customer 2" - customer.type = "Individual" - customer.save() - self.customer2 = customer.name - - if frappe.db.exists("Customer", "_Test PR Customer 3"): - self.customer3 = "_Test PR Customer 3" - else: - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test PR Customer 3" - customer.type = "Individual" - customer.default_currency = "EUR" - customer.save() - self.customer3 = customer.name + self.customer = make_customer("_Test PR Customer") + self.customer2 = make_customer("_Test PR Customer 2") + self.customer3 = make_customer("_Test PR Customer 3", "EUR") + self.customer4 = make_customer("_Test PR Customer 4", "EUR") + self.customer5 = make_customer("_Test PR Customer 5", "EUR") def create_account(self): account_name = "Debtors EUR" @@ -598,6 +576,156 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.payments[0].amount, amount) self.assertEqual(pr.payments[0].currency, "EUR") + def test_difference_amount_via_journal_entry(self): + # Make Sale Invoice + si = self.create_sales_invoice( + qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + si.customer = self.customer4 + si.currency = "EUR" + si.conversion_rate = 85 + si.debit_to = self.debtors_eur + si.save().submit() + + # Make payment using Journal Entry + je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate()) + je1.multi_currency = 1 + je1.accounts[0].exchange_rate = 1 + je1.accounts[0].credit_in_account_currency = 0 + je1.accounts[0].credit = 0 + je1.accounts[0].debit_in_account_currency = 8000 + je1.accounts[0].debit = 8000 + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = self.customer4 + je1.accounts[1].exchange_rate = 80 + je1.accounts[1].credit_in_account_currency = 100 + je1.accounts[1].credit = 8000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je1.save() + je1.submit() + + je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate()) + je2.multi_currency = 1 + je2.accounts[0].exchange_rate = 1 + je2.accounts[0].credit_in_account_currency = 0 + je2.accounts[0].credit = 0 + je2.accounts[0].debit_in_account_currency = 16000 + je2.accounts[0].debit = 16000 + je2.accounts[1].party_type = "Customer" + je2.accounts[1].party = self.customer4 + je2.accounts[1].exchange_rate = 80 + je2.accounts[1].credit_in_account_currency = 200 + je1.accounts[1].credit = 16000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je2.save() + je2.submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer4 + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + # Test exact payment allocation + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Test partial payment allocation (with excess payment entry) + pr.set("allocation", []) + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[1].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR" + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Check if difference journal entry gets generated for difference amount after reconciliation + pr.reconcile() + total_debit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, + "sum(debit) as amount", + group_by="reference_name", + )[0].amount + + self.assertEqual(flt(total_debit_amount, 2), -500) + + def test_difference_amount_via_payment_entry(self): + # Make Sale Invoice + si = self.create_sales_invoice( + qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + si.customer = self.customer5 + si.currency = "EUR" + si.conversion_rate = 85 + si.debit_to = self.debtors_eur + si.save().submit() + + # Make payment using Payment Entry + pe1 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer5, + paid_from=self.debtors_eur, + paid_to=self.bank, + paid_amount=100, + ) + + pe1.source_exchange_rate = 80 + pe1.received_amount = 8000 + pe1.save() + pe1.submit() + + pe2 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer5, + paid_from=self.debtors_eur, + paid_to=self.bank, + paid_amount=200, + ) + + pe2.source_exchange_rate = 80 + pe2.received_amount = 16000 + pe2.save() + pe2.submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer5 + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + pr.set("allocation", []) + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[1].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + def test_differing_cost_center_on_invoice_and_payment(self): """ Cost Center filter should not affect outstanding amount calculation @@ -618,3 +746,17 @@ class TestPaymentReconciliation(FrappeTestCase): # check PR tool output self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("payments")), 0) + + +def make_customer(customer_name, currency=None): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.type = "Individual" + + if currency: + customer.default_currency = currency + customer.save() + return customer.name + else: + return customer_name diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 6a21692c6ac..0f7e47acfee 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -20,7 +20,9 @@ "section_break_5", "difference_amount", "column_break_7", - "difference_account" + "difference_account", + "exchange_rate", + "currency" ], "fields": [ { @@ -37,7 +39,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Allocated Amount", - "options": "Currency", + "options": "currency", "reqd": 1 }, { @@ -112,7 +114,7 @@ "fieldtype": "Currency", "hidden": 1, "label": "Unreconciled Amount", - "options": "Currency", + "options": "currency", "read_only": 1 }, { @@ -120,7 +122,7 @@ "fieldtype": "Currency", "hidden": 1, "label": "Amount", - "options": "Currency", + "options": "currency", "read_only": 1 }, { @@ -129,11 +131,24 @@ "hidden": 1, "label": "Reference Row", "read_only": 1 + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-10-06 11:48:59.616562", + "modified": "2022-12-24 21:01:14.882747", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", @@ -141,5 +156,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index 00c9e1240c5..c4dbd7e8441 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -11,7 +11,8 @@ "col_break1", "amount", "outstanding_amount", - "currency" + "currency", + "exchange_rate" ], "fields": [ { @@ -62,11 +63,17 @@ "hidden": 1, "label": "Currency", "options": "Currency" + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Exchange Rate" } ], "istable": 1, "links": [], - "modified": "2021-08-24 22:42:40.923179", + "modified": "2022-11-08 18:18:02.502149", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", @@ -75,5 +82,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index add07e870d8..d300ea97abc 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -15,7 +15,8 @@ "difference_amount", "sec_break1", "remark", - "currency" + "currency", + "exchange_rate" ], "fields": [ { @@ -91,11 +92,17 @@ "label": "Difference Amount", "options": "currency", "read_only": 1 + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Exchange Rate" } ], "istable": 1, "links": [], - "modified": "2021-08-30 10:51:48.140062", + "modified": "2022-11-08 18:18:36.268760", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", @@ -103,5 +110,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From 1d045e0458d962e9dd6e1c9d10999e0e38e0f58c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 13:27:40 +0530 Subject: [PATCH 07/19] ci: bump node in release workflow (backport #33574) (#33575) * ci: bump node in release workflow (#33574) [skip ci] (cherry picked from commit 1ad1fc4c7db01c25bce125dc1f5a598394ba7b5c) # Conflicts: # .github/workflows/release.yml * chore: conflicts [skip ci] Co-authored-by: Ankush Menat --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d61caa98708..ccd712065dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v2 with: - node-version: 16 + node-version: 18 - name: Setup dependencies run: | From 1d26d7c077db8bba184abb177531fb5db75a48a0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 22:22:33 +0530 Subject: [PATCH 08/19] chore: patch property setters for JE with new entry type (backport #33569) (#33583) chore: patch property setters for JE with new entry type (cherry picked from commit 789e448f0ebcd8345389e66ab3b27c653bb2c145) Co-authored-by: ruthra kumar --- erpnext/patches.txt | 3 ++- .../update_entry_type_for_journal_entry.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v14_0/update_entry_type_for_journal_entry.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f7d2dedb1b3..1d71a86ef28 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -320,4 +320,5 @@ erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v14_0.update_partial_tds_fields erpnext.patches.v14_0.create_incoterms_and_migrate_shipment erpnext.patches.v14_0.setup_clear_repost_logs -erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request \ No newline at end of file +erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request +erpnext.patches.v14_0.update_entry_type_for_journal_entry diff --git a/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py b/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py new file mode 100644 index 00000000000..bce92555577 --- /dev/null +++ b/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py @@ -0,0 +1,18 @@ +import frappe + + +def execute(): + """ + Update Propery Setters for Journal Entry with new 'Entry Type' + """ + new_voucher_type = "Exchange Gain Or Loss" + prop_setter = frappe.db.get_list( + "Property Setter", + filters={"doc_type": "Journal Entry", "field_name": "voucher_type", "property": "options"}, + ) + if prop_setter: + property_setter_doc = frappe.get_doc("Property Setter", prop_setter[0].get("name")) + + if new_voucher_type not in property_setter_doc.value.split("\n"): + property_setter_doc.value += "\n" + new_voucher_type + property_setter_doc.save() From b96a97f6b4811ad499940a2525357539b9eca46c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 6 Jan 2023 16:21:20 +0530 Subject: [PATCH 09/19] fix: better handling of duplicate bundle items (cherry picked from commit c717e87c9e2723b1b2751afa37ca6b52f95b6001) --- erpnext/stock/doctype/packed_item/packed_item.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index d6067516660..dbd8de4fcb0 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -83,8 +83,8 @@ def reset_packing_list(doc): # 1. items were deleted # 2. if bundle item replaced by another item (same no. of items but different items) # we maintain list to track recurring item rows as well - items_before_save = [item.item_code for item in doc_before_save.get("items")] - items_after_save = [item.item_code for item in doc.get("items")] + items_before_save = [(item.name, item.item_code) for item in doc_before_save.get("items")] + items_after_save = [(item.name, item.item_code) for item in doc.get("items")] reset_table = items_before_save != items_after_save else: # reset: if via Update Items OR From c6c3ac3e55e5a2b4e17793a120c4a9a840a43269 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:11:34 +0530 Subject: [PATCH 10/19] fix: incorrect warehouse and selling amount on bundled products (#33549) fix: incorrect warehouse and selling amount on bundled products (#33549) (cherry picked from commit bbe5e5d9d6c51f0600b49261a2a9f52ebb6ee67e) Co-authored-by: ruthra kumar --- .../report/gross_profit/gross_profit.py | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index ba947f392f8..646fe85bac9 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -439,6 +439,18 @@ class GrossProfitGenerator(object): row.delivery_note, frappe._dict() ) row.item_row = row.dn_detail + # Update warehouse and base_amount from 'Packed Item' List + if product_bundles and not row.parent: + # For Packed Items, row.parent_invoice will be the Bundle name + product_bundle = product_bundles.get(row.parent_invoice) + if product_bundle: + for packed_item in product_bundle: + if ( + packed_item.get("item_code") == row.item_code + and packed_item.get("parent_detail_docname") == row.item_row + ): + row.warehouse = packed_item.warehouse + row.base_amount = packed_item.base_amount # get buying amount if row.item_code in product_bundles: @@ -589,7 +601,9 @@ class GrossProfitGenerator(object): buying_amount = 0.0 for packed_item in product_bundle: if packed_item.get("parent_detail_docname") == row.item_row: - buying_amount += self.get_buying_amount(row, packed_item.item_code) + packed_item_row = row.copy() + packed_item_row.warehouse = packed_item.warehouse + buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -922,12 +936,25 @@ class GrossProfitGenerator(object): def load_product_bundle(self): self.product_bundles = {} - for d in frappe.db.sql( - """select parenttype, parent, parent_item, - item_code, warehouse, -1*qty as total_qty, parent_detail_docname - from `tabPacked Item` where docstatus=1""", - as_dict=True, - ): + pki = qb.DocType("Packed Item") + + pki_query = ( + frappe.qb.from_(pki) + .select( + pki.parenttype, + pki.parent, + pki.parent_item, + pki.item_code, + pki.warehouse, + (-1 * pki.qty).as_("total_qty"), + pki.rate, + (pki.rate * pki.qty).as_("base_amount"), + pki.parent_detail_docname, + ) + .where(pki.docstatus == 1) + ) + + for d in pki_query.run(as_dict=True): self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault( d.parent, frappe._dict() ).setdefault(d.parent_item, []).append(d) From a92b4e7255e173873fa285dc2f5fca414dc601b8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:01:51 +0530 Subject: [PATCH 11/19] fix(stock entry): wrong valuation rate in repack (#33579) fix(stock entry): wrong valuation rate in repack (cherry picked from commit 99f5e869e02d0cba39d3cdd970dd7c63e641eaa1) Co-authored-by: safvanhuzain --- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a5264ac195a..d90a74f7b4a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -998,7 +998,9 @@ class StockEntry(StockController): ) def mark_finished_and_scrap_items(self): - if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): + if self.purpose != "Repack" and any( + [d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)] + ): return finished_item = self.get_finished_item() From d2e3701b1a0221b6638f433f6acfd72fed526e06 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:02:15 +0530 Subject: [PATCH 12/19] fix: Timeout error while saving the purchase invoice (#33577) * fix: timeout error in the Purchase Invoice (cherry picked from commit 7249657d15323ba089f2aa20e77e6bd5aae4ba76) * feat: provision to disable get last purchase rate fix: set_incoming_rate condition (cherry picked from commit d1d4671320cf4f5b86e6cdeb4624e0fd4132caf7) * fix: linters issue (cherry picked from commit 05df8579cd65b04fc10c9904b53832bf360d8457) * test: test case to check disable last purchase rate (cherry picked from commit ec171fc7c13f39f4a896c28ad8173fccbc888289) Co-authored-by: Rohit Waghchaure --- .../buying_settings/buying_settings.json | 19 +++++--- erpnext/controllers/buying_controller.py | 11 ++++- erpnext/stock/doctype/item/item.py | 9 ++-- .../purchase_receipt/test_purchase_receipt.py | 43 +++++++++++++++++++ erpnext/stock/get_item_details.py | 7 ++- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 28158a31b94..34417f7ac3a 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -9,8 +9,8 @@ "supplier_and_price_defaults_section", "supp_master_name", "supplier_group", - "column_break_4", "buying_price_list", + "column_break_4", "maintain_same_rate_action", "role_to_override_stop_action", "transaction_settings_section", @@ -20,6 +20,7 @@ "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", + "disable_last_purchase_rate", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -71,7 +72,7 @@ }, { "fieldname": "subcontract", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Subcontracting Settings" }, { @@ -118,8 +119,8 @@ }, { "fieldname": "supplier_and_price_defaults_section", - "fieldtype": "Section Break", - "label": "Supplier and Price Defaults" + "fieldtype": "Tab Break", + "label": "Naming Series and Price Defaults" }, { "fieldname": "column_break_4", @@ -127,12 +128,18 @@ }, { "fieldname": "transaction_settings_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Transaction Settings" }, { "fieldname": "column_break_12", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disable_last_purchase_rate", + "fieldtype": "Check", + "label": "Disable Last Purchase Rate" } ], "icon": "fa fa-cog", @@ -140,7 +147,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-09-27 10:50:27.050252", + "modified": "2023-01-09 17:08:28.828173", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 2efa5457368..445620a1246 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -278,6 +278,9 @@ class BuyingController(SubcontractingController): if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): return + if not self.is_internal_transfer(): + return + ref_doctype_map = { "Purchase Order": "Sales Order Item", "Purchase Receipt": "Delivery Note Item", @@ -548,7 +551,9 @@ class BuyingController(SubcontractingController): self.process_fixed_asset() self.update_fixed_asset(field) - if self.doctype in ["Purchase Order", "Purchase Receipt"]: + if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): update_last_purchase_rate(self, is_submit=1) def on_cancel(self): @@ -557,7 +562,9 @@ class BuyingController(SubcontractingController): if self.get("is_return"): return - if self.doctype in ["Purchase Order", "Purchase Receipt"]: + if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): update_last_purchase_rate(self, is_submit=0) if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 20bc9d9b2c9..423b9defc19 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -164,10 +164,7 @@ class Item(Document): if not self.is_stock_item or self.has_serial_no or self.has_batch_no: return - if not self.valuation_rate and self.standard_rate: - self.valuation_rate = self.standard_rate - - if not self.valuation_rate and not self.is_customer_provided_item: + if not self.valuation_rate and not self.standard_rate and not self.is_customer_provided_item: frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -192,7 +189,7 @@ class Item(Document): item_code=self.name, target=default_warehouse, qty=self.opening_stock, - rate=self.valuation_rate, + rate=self.valuation_rate or self.standard_rate, company=default.company, posting_date=getdate(), posting_time=nowtime(), @@ -279,7 +276,7 @@ class Item(Document): frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate")) def update_template_tables(self): - template = frappe.get_doc("Item", self.variant_of) + template = frappe.get_cached_doc("Item", self.variant_of) # add item taxes from template for d in template.get("taxes"): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index dc9f2b21177..b6341466f87 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1501,6 +1501,49 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertTrue(return_pi.docstatus == 1) + def test_disable_last_purchase_rate(self): + from erpnext.stock.get_item_details import get_item_details + + item = make_item( + "_Test Disable Last Purchase Rate", + {"is_purchase_item": 1, "is_stock_item": 1}, + ) + + frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 1) + + pr = make_purchase_receipt( + qty=1, + rate=100, + item_code=item.name, + ) + + args = pr.items[0].as_dict() + args.update( + { + "supplier": pr.supplier, + "doctype": pr.doctype, + "conversion_rate": pr.conversion_rate, + "currency": pr.currency, + "company": pr.company, + "posting_date": pr.posting_date, + "posting_time": pr.posting_time, + } + ) + + res = get_item_details(args) + self.assertEqual(res.get("last_purchase_rate"), 0) + + frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 0) + + pr = make_purchase_receipt( + qty=1, + rate=100, + item_code=item.name, + ) + + res = get_item_details(args) + self.assertEqual(res.get("last_purchase_rate"), 100) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8561dc2e91e..f7fcb30acd2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -411,7 +411,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): args.stock_qty = out.stock_qty # calculate last purchase rate - if args.get("doctype") in purchase_doctypes: + if args.get("doctype") in purchase_doctypes and not frappe.db.get_single_value( + "Buying Settings", "disable_last_purchase_rate" + ): from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate out.last_purchase_rate = item_last_purchase_rate( @@ -813,6 +815,9 @@ def get_price_list_rate(args, item_doc, out=None): flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) ) + if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): + return out + if not out.price_list_rate and args.transaction_type == "buying": from erpnext.stock.doctype.item.item import get_last_purchase_details From 914e2fdded1bfa155b3100b22ac68d762415063c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:02:50 +0530 Subject: [PATCH 13/19] fix: customer/supplier quick entry dialog (#33496) fix: customer/supplier quick entry dialog (#33496) * fix: readonly primary contact fields. * refactor: supplier and customer quick entry form into common class. (cherry picked from commit 6bc8bb26b693182dbcdd296cfe2cd04b0d1fdf3d) Co-authored-by: Devin Slauenwhite --- erpnext/public/js/erpnext.bundle.js | 1 + .../js/utils/contact_address_quick_entry.js | 100 ++++++++++++++++++ .../public/js/utils/customer_quick_entry.js | 80 +------------- .../public/js/utils/supplier_quick_entry.js | 76 +------------ 4 files changed, 103 insertions(+), 154 deletions(-) create mode 100644 erpnext/public/js/utils/contact_address_quick_entry.js diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 14a088e405c..7b230af2699 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -13,6 +13,7 @@ import "./help_links"; import "./agriculture/ternary_plot"; import "./templates/item_quick_entry.html"; import "./utils/item_quick_entry"; +import "./utils/contact_address_quick_entry"; import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js new file mode 100644 index 00000000000..adabf08c203 --- /dev/null +++ b/erpnext/public/js/utils/contact_address_quick_entry.js @@ -0,0 +1,100 @@ +frappe.provide('frappe.ui.form'); + +frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm extends frappe.ui.form.QuickEntryForm { + constructor(doctype, after_insert, init_callback, doc, force) { + super(doctype, after_insert, init_callback, doc, force); + this.skip_redirect_on_error = true; + } + + render_dialog() { + this.mandatory = this.mandatory.concat(this.get_variant_fields()); + super.render_dialog(); + } + + insert() { + /** + * Using alias fieldnames because the doctype definition define "email_id" and "mobile_no" as readonly fields. + * Therefor, resulting in the fields being "hidden". + */ + const map_field_names = { + "email_address": "email_id", + "mobile_number": "mobile_no", + }; + + Object.entries(map_field_names).forEach(([fieldname, new_fieldname]) => { + this.dialog.doc[new_fieldname] = this.dialog.doc[fieldname]; + delete this.dialog.doc[fieldname]; + }); + + return super.insert(); + } + + get_variant_fields() { + var variant_fields = [{ + fieldtype: "Section Break", + label: __("Primary Contact Details"), + collapsible: 1 + }, + { + label: __("Email Id"), + fieldname: "email_address", + fieldtype: "Data", + options: "Email", + }, + { + fieldtype: "Column Break" + }, + { + label: __("Mobile Number"), + fieldname: "mobile_number", + fieldtype: "Data" + }, + { + fieldtype: "Section Break", + label: __("Primary Address Details"), + collapsible: 1 + }, + { + label: __("Address Line 1"), + fieldname: "address_line1", + fieldtype: "Data" + }, + { + label: __("Address Line 2"), + fieldname: "address_line2", + fieldtype: "Data" + }, + { + label: __("ZIP Code"), + fieldname: "pincode", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("City"), + fieldname: "city", + fieldtype: "Data" + }, + { + label: __("State"), + fieldname: "state", + fieldtype: "Data" + }, + { + label: __("Country"), + fieldname: "country", + fieldtype: "Link", + options: "Country" + }, + { + label: __("Customer POS Id"), + fieldname: "customer_pos_id", + fieldtype: "Data", + hidden: 1 + }]; + + return variant_fields; + } +} diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js index d2c5c721cc4..b2532085f65 100644 --- a/erpnext/public/js/utils/customer_quick_entry.js +++ b/erpnext/public/js/utils/customer_quick_entry.js @@ -1,81 +1,3 @@ frappe.provide('frappe.ui.form'); -frappe.ui.form.CustomerQuickEntryForm = class CustomerQuickEntryForm extends frappe.ui.form.QuickEntryForm { - constructor(doctype, after_insert, init_callback, doc, force) { - super(doctype, after_insert, init_callback, doc, force); - this.skip_redirect_on_error = true; - } - - render_dialog() { - this.mandatory = this.mandatory.concat(this.get_variant_fields()); - super.render_dialog(); - } - - get_variant_fields() { - var variant_fields = [{ - fieldtype: "Section Break", - label: __("Primary Contact Details"), - collapsible: 1 - }, - { - label: __("Email Id"), - fieldname: "email_id", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("Mobile Number"), - fieldname: "mobile_no", - fieldtype: "Data" - }, - { - fieldtype: "Section Break", - label: __("Primary Address Details"), - collapsible: 1 - }, - { - label: __("Address Line 1"), - fieldname: "address_line1", - fieldtype: "Data" - }, - { - label: __("Address Line 2"), - fieldname: "address_line2", - fieldtype: "Data" - }, - { - label: __("ZIP Code"), - fieldname: "pincode", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("City"), - fieldname: "city", - fieldtype: "Data" - }, - { - label: __("State"), - fieldname: "state", - fieldtype: "Data" - }, - { - label: __("Country"), - fieldname: "country", - fieldtype: "Link", - options: "Country" - }, - { - label: __("Customer POS Id"), - fieldname: "customer_pos_id", - fieldtype: "Data", - hidden: 1 - }]; - - return variant_fields; - } -} +frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm; diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js index 8d591a96510..687b01454a2 100644 --- a/erpnext/public/js/utils/supplier_quick_entry.js +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -1,77 +1,3 @@ frappe.provide('frappe.ui.form'); -frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends frappe.ui.form.QuickEntryForm { - constructor(doctype, after_insert, init_callback, doc, force) { - super(doctype, after_insert, init_callback, doc, force); - this.skip_redirect_on_error = true; - } - - render_dialog() { - this.mandatory = this.mandatory.concat(this.get_variant_fields()); - super.render_dialog(); - } - - get_variant_fields() { - var variant_fields = [ - { - fieldtype: "Section Break", - label: __("Primary Contact Details"), - collapsible: 1 - }, - { - label: __("Email Id"), - fieldname: "email_id", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("Mobile Number"), - fieldname: "mobile_no", - fieldtype: "Data" - }, - { - fieldtype: "Section Break", - label: __("Primary Address Details"), - collapsible: 1 - }, - { - label: __("Address Line 1"), - fieldname: "address_line1", - fieldtype: "Data" - }, - { - label: __("Address Line 2"), - fieldname: "address_line2", - fieldtype: "Data" - }, - { - label: __("ZIP Code"), - fieldname: "pincode", - fieldtype: "Data" - }, - { - fieldtype: "Column Break" - }, - { - label: __("City"), - fieldname: "city", - fieldtype: "Data" - }, - { - label: __("State"), - fieldname: "state", - fieldtype: "Data" - }, - { - label: __("Country"), - fieldname: "country", - fieldtype: "Link", - options: "Country" - } - ]; - - return variant_fields; - } -}; +frappe.ui.form.SupplierQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm; From f5015750e4451201c9f9b444a1e2590e8fb892eb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 10 Jan 2023 09:54:15 +0530 Subject: [PATCH 14/19] perf: Drop `name` part from posting sort index (#33551) (cherry picked from commit 8a56df695d9fd26f7c78ea0ebb86501ae8b6b1d1) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 5 +++++ .../patches/v13_0/drop_unused_sle_index_parts.py | 14 ++++++++++++++ .../stock_ledger_entry/stock_ledger_entry.py | 11 +++-------- 3 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 erpnext/patches/v13_0/drop_unused_sle_index_parts.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1d71a86ef28..3e6e6e008be 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,6 +315,11 @@ erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger +<<<<<<< HEAD +======= +erpnext.patches.v13_0.update_schedule_type_in_loans +erpnext.patches.v13_0.drop_unused_sle_index_parts +>>>>>>> 8a56df695d (perf: Drop `name` part from posting sort index (#33551)) erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v14_0.update_partial_tds_fields diff --git a/erpnext/patches/v13_0/drop_unused_sle_index_parts.py b/erpnext/patches/v13_0/drop_unused_sle_index_parts.py new file mode 100644 index 00000000000..fa8a98ce16c --- /dev/null +++ b/erpnext/patches/v13_0/drop_unused_sle_index_parts.py @@ -0,0 +1,14 @@ +import frappe + +from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import on_doctype_update + + +def execute(): + try: + frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`") + except Exception: + frappe.log_error("Failed to drop index") + return + + # Recreate indexes + on_doctype_update() 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 c64370dcdf2..052f7781c13 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -221,14 +221,9 @@ class StockLedgerEntry(Document): def on_doctype_update(): - if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): - frappe.db.commit() - frappe.db.add_index( - "Stock Ledger Entry", - fields=["posting_date", "posting_time", "name"], - index_name="posting_sort_index", - ) - + frappe.db.add_index( + "Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index" + ) 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", ["warehouse", "item_code"], "item_warehouse") From 44a95da8ab7cd97cf26bf4bc5b55b5b8309895cb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:55:12 +0530 Subject: [PATCH 15/19] fix(accounts): currency fields no longer read as strings by validation function in Payment Entry (#33535) fix(accounts): currency fields no longer read as strings by validation function in Payment Entry (#33535) explicitly cast paid_amount and received_amount to float in the Payment Entry set_unallocated_amount validation function (cherry picked from commit 4d5067d6d4e187feacc4b02d22de5c5f3ef4d051) Co-authored-by: Gughan Ravikumar --- 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 5dbc91654f9..ab458caeb5d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -622,7 +622,7 @@ class PaymentEntry(AccountsController): self.payment_type == "Receive" and self.base_total_allocated_amount < self.base_received_amount + total_deductions and self.total_allocated_amount - < self.paid_amount + (total_deductions / self.source_exchange_rate) + < flt(self.paid_amount) + (total_deductions / self.source_exchange_rate) ): self.unallocated_amount = ( self.base_received_amount + total_deductions - self.base_total_allocated_amount @@ -632,7 +632,7 @@ class PaymentEntry(AccountsController): self.payment_type == "Pay" and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) and self.total_allocated_amount - < self.received_amount + (total_deductions / self.target_exchange_rate) + < flt(self.received_amount) + (total_deductions / self.target_exchange_rate) ): self.unallocated_amount = ( self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) From 955b487296b5d63e79bc17e9a3129521e54b60a8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Jan 2023 10:44:38 +0530 Subject: [PATCH 16/19] chore: resolve conflicts --- erpnext/patches.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3e6e6e008be..f4b1a06aea8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,11 +315,7 @@ erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger -<<<<<<< HEAD -======= -erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v13_0.drop_unused_sle_index_parts ->>>>>>> 8a56df695d (perf: Drop `name` part from posting sort index (#33551)) erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v14_0.update_partial_tds_fields From ab0a2b427291cd381f885dc46b62dcd67bada195 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 10 Jan 2023 10:59:50 +0530 Subject: [PATCH 17/19] fix: don't check other warehouse ledgers to calculate valuation rate (cherry picked from commit ef2bf3c22350351c8ddbc375c4d04a1f48854ed5) --- erpnext/stock/stock_ledger.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 55a11a18671..5d75bfd05a3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1270,20 +1270,6 @@ def get_valuation_rate( (item_code, warehouse, voucher_no, voucher_type), ) - if not last_valuation_rate: - # Get valuation rate from last sle for the item against any warehouse - last_valuation_rate = frappe.db.sql( - """select valuation_rate - from `tabStock Ledger Entry` force index (item_code) - where - item_code = %s - AND valuation_rate > 0 - AND is_cancelled = 0 - AND NOT(voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", - (item_code, voucher_no, voucher_type), - ) - if last_valuation_rate: return flt(last_valuation_rate[0][0]) From e995e952b576a0aa1e4415e7dda78ea4518bca49 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 22:18:59 +0530 Subject: [PATCH 18/19] fix: Incorrect exchange rate in payment entries (#33481) fix: Incorrect exchange rate in payment entries (#33481) * fix: Incorrect exchange rate in payment entries * test: Update failing tests (cherry picked from commit 0ed938a49020d1d892d9727fbeac5a88b996e71c) Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- .../buying/doctype/purchase_order/test_purchase_order.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ab458caeb5d..bc2a1e50793 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -247,7 +247,7 @@ class PaymentEntry(AccountsController): self.set_target_exchange_rate(ref_doc) def set_source_exchange_rate(self, ref_doc=None): - if self.paid_from and not self.source_exchange_rate: + if self.paid_from: if self.paid_from_account_currency == self.company_currency: self.source_exchange_rate = 1 else: diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 291d756a415..572d9d3865c 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -743,9 +743,9 @@ class TestPurchaseOrder(FrappeTestCase): pe = get_payment_entry("Purchase Order", po_doc.name) pe.mode_of_payment = "Cash" pe.paid_from = "Cash - _TC" - pe.source_exchange_rate = 80 - pe.target_exchange_rate = 1 - pe.paid_amount = po_doc.grand_total + pe.source_exchange_rate = 1 + pe.target_exchange_rate = 80 + pe.paid_amount = po_doc.base_grand_total pe.save(ignore_permissions=True) pe.submit() From 34df9ab7d52349f9b071609e1d4bc8d4acea88d7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 22:19:24 +0530 Subject: [PATCH 19/19] fix: RFQ emails not sent with pdf attachment (#33604) fix: RFQ emails not sent with pdf attachment (#33604) (cherry picked from commit e0f5ecdad6d181ce3c69afc2cab7df832e27baa4) Co-authored-by: Smit Vora --- .../doctype/request_for_quotation/request_for_quotation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 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 dbc36449570..8e9ded98421 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -216,6 +216,7 @@ class RequestforQuotation(BuyingController): recipients=data.email_id, sender=sender, attachments=attachments, + print_format=self.meta.default_print_format or "Standard", send_email=True, doctype=self.doctype, name=self.name, @@ -224,9 +225,7 @@ class RequestforQuotation(BuyingController): frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier)) def get_attachments(self): - attachments = [d.name for d in get_attachments(self.doctype, self.name)] - attachments.append(frappe.attach_print(self.doctype, self.name, doc=self)) - return attachments + return [d.name for d in get_attachments(self.doctype, self.name)] def update_rfq_supplier_status(self, sup_name=None): for supplier in self.suppliers: