From 867ed3a1e421f251262c30517f132f13f696ccc2 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 22 Jun 2021 16:53:35 +0530 Subject: [PATCH 01/48] feat: add provision for process loss in manufac --- erpnext/manufacturing/doctype/bom/bom.py | 7 +- .../bom_scrap_item/bom_scrap_item.json | 429 ++++-------------- .../stock/doctype/stock_entry/stock_entry.py | 23 +- .../stock_entry_detail.json | 9 +- 4 files changed, 133 insertions(+), 335 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d1f63854c71..00e79d4f091 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -702,8 +702,11 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) elif fetch_scrap_items: - query = query.format(table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description", is_stock_item=is_stock_item, qty_field="stock_qty") + query = query.format( + table="BOM Scrap Item", where_conditions="", + select_columns=", bom_item.idx, item.description, is_process_loss", + is_stock_item=is_stock_item, qty_field="stock_qty" + ) items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) else: 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 9f7091dd8d7..7018082e402 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json @@ -1,345 +1,112 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-26 02:19:21.642081", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-09-26 02:19:21.642081", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "is_process_loss", + "quantity_and_rate", + "stock_qty", + "rate", + "amount", + "column_break_6", + "stock_uom", + "base_rate", + "base_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quantity_and_rate", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Quantity and Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "quantity_and_rate", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amount", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Stock UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Basic Rate (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "base_rate", + "fieldtype": "Currency", + "label": "Basic Rate (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Basic Amount (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "base_amount", + "fieldtype": "Currency", + "label": "Basic Amount (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-07-04 16:04:32.442287", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Scrap Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-06-22 16:46:12.153311", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Scrap Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 66f8b63cb92..256748a8274 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -462,7 +462,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss) # Set basic rate for incoming items for d in self.get('items'): @@ -483,6 +483,8 @@ class StockEntry(StockController): raise_error_if_no_rate=raise_error_if_no_rate) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) + if d.is_process_loss: + d.basic_rate = flt(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): @@ -1034,6 +1036,7 @@ class StockEntry(StockController): self.set_scrap_items() self.set_actual_qty() + self.adjust_qty_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount() @@ -1345,6 +1348,7 @@ class StockEntry(StockController): get_default_cost_center(item_dict[d], company = self.company)) se_child.is_finished_item = item_dict[d].get("is_finished_item", 0) se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) + se_child.is_process_loss = item_dict[d].get("is_process_loss", 0) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name", "serial_no", "batch_no"]: @@ -1523,6 +1527,23 @@ class StockEntry(StockController): if material_request and material_request not in material_requests: material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + + def adjust_qty_for_process_loss(self): + process_loss_dict = {} + for d in self.get("items"): + if not d.is_process_loss: + continue + 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 FG has 1 row. + d.transfer_qty -= process_loss_dict[d.item_code][0] + d.qty -= process_loss_dict[d.item_code][1] @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 864ff488b22..7725881c07a 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -18,6 +18,7 @@ "col_break2", "is_finished_item", "is_scrap_item", + "is_process_loss", "subcontracted_item", "section_break_8", "description", @@ -542,13 +543,19 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-11 13:47:50.158754", + "modified": "2021-06-22 16:47:11.268975", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 025f4b21de56ae6c69fa61296ec80377174b6c8f Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Jun 2021 15:06:00 +0530 Subject: [PATCH 02/48] feat: add is process loss autoset and validation --- erpnext/manufacturing/doctype/bom/bom.js | 15 +++++++++++++++ erpnext/manufacturing/doctype/bom/bom.py | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index a09a5e34300..5cdd1373913 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -360,6 +360,14 @@ frappe.ui.form.on("BOM", { } }); +function set_is_process_loss(doc, cdt, cdn) { + const row = locals[cdt][cdn] + if (row.item_code === doc.item) { + row.is_process_loss = 1 + frappe.msgprint(__("Item:") + ` ${row.item_code} ` + __("set as process loss.")) + } +} + erpnext.bom.BomController = erpnext.TransactionController.extend({ conversion_rate: function(doc) { if(this.frm.doc.currency === this.get_company_currency()) { @@ -380,6 +388,9 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ child.bom_no = ''; } + if (scrap_items) { + set_is_process_loss(doc, cdt, cdn) + } get_bom_material_detail(doc, cdt, cdn, scrap_items); }, @@ -447,6 +458,10 @@ 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"); refresh_field("scrap_items"); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 00e79d4f091..3402a6b3489 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -81,6 +81,7 @@ class BOM(WebsiteGenerator): self.validate_operations() self.calculate_cost() self.update_stock_qty() + self.validate_scrap_items() self.update_cost(update_parent=False, from_child_bom=True, save=False) def get_context(self, context): @@ -585,6 +586,15 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + def validate_scrap_items(self): + for item in self.scrap_items: + if item.item_code == self.item and not item.is_process_loss: + frappe.throw(_('Item:') + f' {item.item_code} ' +\ + _('in Scrap/Loss Items table should have Is Process Loss checked.')) + elif item.item_code != self.item and item.is_process_loss: + frappe.throw(_('Item:') + f' {item.item_code} ' +\ + _('in Scrap/Loss Items table should not have Is Process Loss checked.')) + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) From 8032095d819a87eaf16a2415eb696c2f7fc17a7e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Jun 2021 15:30:48 +0530 Subject: [PATCH 03/48] fix: add warehouse and unset is scrap for process loss items --- erpnext/stock/doctype/stock_entry/stock_entry.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 256748a8274..4cdfcc47bdb 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1036,7 +1036,7 @@ class StockEntry(StockController): self.set_scrap_items() self.set_actual_qty() - self.adjust_qty_for_process_loss() + self.update_items_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount() @@ -1528,11 +1528,17 @@ class StockEntry(StockController): material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) - def adjust_qty_for_process_loss(self): + 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) @@ -1541,10 +1547,11 @@ class StockEntry(StockController): for d in self.get("items"): if not d.is_finished_item or d.item_code not in process_loss_dict: continue - # Assumption: 1 FG has 1 row. + # 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] + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): From 697a8bce7f81fe9515ca6c849d4393429acd4bd5 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 24 Jun 2021 15:05:52 +0530 Subject: [PATCH 04/48] refactor: shift auto entry of is process loss check, update validations --- erpnext/manufacturing/doctype/bom/bom.js | 50 +++++++++++++++++------- erpnext/manufacturing/doctype/bom/bom.py | 22 +++++++++-- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 5cdd1373913..a8b2e7fc675 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -360,14 +360,6 @@ frappe.ui.form.on("BOM", { } }); -function set_is_process_loss(doc, cdt, cdn) { - const row = locals[cdt][cdn] - if (row.item_code === doc.item) { - row.is_process_loss = 1 - frappe.msgprint(__("Item:") + ` ${row.item_code} ` + __("set as process loss.")) - } -} - erpnext.bom.BomController = erpnext.TransactionController.extend({ conversion_rate: function(doc) { if(this.frm.doc.currency === this.get_company_currency()) { @@ -388,9 +380,6 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ child.bom_no = ''; } - if (scrap_items) { - set_is_process_loss(doc, cdt, cdn) - } get_bom_material_detail(doc, cdt, cdn, scrap_items); }, @@ -459,9 +448,10 @@ 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 + r.message.rate = 0; + r.message.base_rate = 0; } + $.extend(d, r.message); refresh_field("items"); refresh_field("scrap_items"); @@ -677,4 +667,36 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) { frm.set_value("operations", []); } toggle_operations(frm); -}); \ No newline at end of file +}); + +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) + } + }, +}); + +function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) { + frappe.prompt( + { + fieldname: "percent", + fieldtype: "Percent", + label: __("% Finished Item Quantity"), + description: + __("Set quantity of process loss item:") + + ` ${item_code} ` + + __("as a percentage of finished item quantity"), + }, + (data) => { + const row = locals[cdt][cdn]; + row.stock_qty = (frm.doc.quantity * data.percent) / 100; + row.qty = row.stock_qty / (row.conversion_factor ?? 1); + refresh_field("scrap_items"); + }, + __("Set Process Loss Item Quantity"), + __("Set Quantity") + ); +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3402a6b3489..e2a556f85c1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -589,11 +589,25 @@ class BOM(WebsiteGenerator): def validate_scrap_items(self): for item in self.scrap_items: if item.item_code == self.item and not item.is_process_loss: - frappe.throw(_('Item:') + f' {item.item_code} ' +\ - _('in Scrap/Loss Items table should have Is Process Loss checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ + _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) elif item.item_code != self.item and item.is_process_loss: - frappe.throw(_('Item:') + f' {item.item_code} ' +\ - _('in Scrap/Loss Items table should not have Is Process Loss checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ + _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) + + stock_uom = item.stock_uom + must_be_whole_number = frappe.get_value("UOM", stock_uom, "must_be_whole_number") + if item.is_process_loss and must_be_whole_number: + frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' +\ + _('with Stock UOM:') + f' {frappe.bold(stock_uom)} '+\ + _('cannot be a Scrap/Loss Item.')) + + if item.is_process_loss and (item.stock_qty >= self.quantity): + frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' +\ + _('should have') +' '+frappe.bold(_('Qty')) +\ + ' ' + _('less than finished goods') + ' ' +\ + frappe.bold(_('Quantity.'))) + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': From f34f0a40c4d476dd38c1e24bd05f51093468938e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 14:20:22 +0530 Subject: [PATCH 05/48] test: add bom tests for process loss val, add se test for qty calc --- erpnext/manufacturing/doctype/bom/test_bom.py | 74 +++++++++++++++++++ .../tests/test_stock_entry_for_manufacture.js | 27 +++++++ 2 files changed, 101 insertions(+) create mode 100644 erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 42b23f223d2..3257716c766 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -227,6 +227,39 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + def test_bom_with_process_loss_item(self): + fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() + + if frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001") is None: + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, 0.25, 0, 1 + ) + bom_doc.submit() + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, 2, 0 + ) + # PL Item qty can't be >= FG Item qty + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, 1, 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, 0.25, 0 + ) + # 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, 0.25, 0, is_process_loss=0 + ) + # FG Items in Scrap/Loss Table should have Is Process Loss set + self.assertRaises(frappe.ValidationError, bom_doc.submit) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) @@ -245,3 +278,44 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non for warehouse in warehouse_list: 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): + bom_doc = frappe.new_doc("BOM") + bom_doc.item = fg_item.item_code + bom_doc.quantity = fg_qty + bom_doc.append("items", { + "item_code": bom_item.item_code, + "qty": 1, + "uom": bom_item.stock_uom, + "stock_uom": bom_item.stock_uom, + "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, + "is_process_loss": is_process_loss + }) + return bom_doc + +def create_process_loss_bom_items(): + item_list = [ + ("_Test Item - Non Whole UOM", "Kg"), + ("_Test Item - Whole UOM", "Unit"), + ( "_Test PL BOM Item", "Unit") + ] + return [create_process_loss_bom_item(it) for it in item_list] + +def create_process_loss_bom_item(item_tuple): + item_code, stock_uom = item_tuple + if frappe.db.exists("Item", item_code) is None: + return make_item( + item_code, + {'stock_uom':stock_uom, 'valuation_rate':100} + ) + else: + return frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js new file mode 100644 index 00000000000..d74f31672d1 --- /dev/null +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js @@ -0,0 +1,27 @@ +QUnit.module('Stock'); + +QUnit.test("test manufacture from bom", function(assert) { + assert.expect(2); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make("Stock Entry", [ + {purpose:"Manufacture"}, + {from_bom:1}, + {bom_no:"BOM-_Test Item - Non Whole UOM-001"}, + {fg_completed_qty:2} + ]); + }, + () => cur_frm.save(), + () => frappe.click_button("Update Rate and Availability"), + () => { + assert.ok(cur_frm.doc.items[1] === 0.75, " Finished Item Qty correct"); + assert.ok(cur_frm.doc.items[2] === 0.25, " Process Loss Item Qty correct"); + }, + () => frappe.tests.click_button('Submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(0.3), + () => done() + ]); +}); + From e957c026bb4cb775c697d1813341ba8d0a971791 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 14:21:12 +0530 Subject: [PATCH 06/48] fix: add more validations, remove source wh req for pl item --- erpnext/manufacturing/doctype/bom/bom.py | 29 ++++++++++--------- .../stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e2a556f85c1..27856b5f317 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -589,25 +589,28 @@ class BOM(WebsiteGenerator): def validate_scrap_items(self): for item in self.scrap_items: if item.item_code == self.item and not item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ - _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + + _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) elif item.item_code != self.item and item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' +\ - _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked.')) + frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + + _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) - stock_uom = item.stock_uom - must_be_whole_number = frappe.get_value("UOM", stock_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: - frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' +\ - _('with Stock UOM:') + f' {frappe.bold(stock_uom)} '+\ - _('cannot be a Scrap/Loss Item.')) + frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' + + _('with Stock UOM:') + f' {frappe.bold(item.stock_uom)} ' + + _('cannot be a Scrap/Loss Item')) if item.is_process_loss and (item.stock_qty >= self.quantity): - frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' +\ - _('should have') +' '+frappe.bold(_('Qty')) +\ - ' ' + _('less than finished goods') + ' ' +\ - frappe.bold(_('Quantity.'))) + frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + + _('should have') +' '+frappe.bold(_('Qty')) + ' ' + + _('less than finished goods') + ' ' + frappe.bold(_('Quantity'))) + if item.is_process_loss and (item.rate > 0): + frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + + _('should have') + ' ' + frappe.bold(_('Rate')) + + ' ' + _('set to 0 because') + ' ' + + frappe.bold(_('Is Process Loss')) + ' ' + _('is checked')) def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4cdfcc47bdb..c9070ffa268 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -333,7 +333,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.is_finished_item or d.is_scrap_item: + if d.is_finished_item or d.is_scrap_item or d.is_process_loss: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) From 69d5e2a6f8fe6ab469b18b6aa9a972dff57be73e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 14:46:08 +0530 Subject: [PATCH 07/48] fix: sider --- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../stock_entry/tests/test_stock_entry_for_manufacture.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 3257716c766..983b7b5e254 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -306,7 +306,7 @@ def create_process_loss_bom_items(): item_list = [ ("_Test Item - Non Whole UOM", "Kg"), ("_Test Item - Whole UOM", "Unit"), - ( "_Test PL BOM Item", "Unit") + ("_Test PL BOM Item", "Unit") ] return [create_process_loss_bom_item(it) for it in item_list] diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js index d74f31672d1..285ae4f59e8 100644 --- a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js @@ -6,10 +6,10 @@ QUnit.test("test manufacture from bom", function(assert) { frappe.run_serially([ () => { return frappe.tests.make("Stock Entry", [ - {purpose:"Manufacture"}, - {from_bom:1}, - {bom_no:"BOM-_Test Item - Non Whole UOM-001"}, - {fg_completed_qty:2} + { purpose: "Manufacture" }, + { from_bom: 1 }, + { bom_no: "BOM-_Test Item - Non Whole UOM-001" }, + { fg_completed_qty: 2 } ]); }, () => cur_frm.save(), From e81a7cf44eb4cece134a12edbb8510e0514b1499 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 15:14:55 +0530 Subject: [PATCH 08/48] refactor: polyfill ?? --- erpnext/manufacturing/doctype/bom/bom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index a8b2e7fc675..35b7801890c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -693,7 +693,7 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) { (data) => { const row = locals[cdt][cdn]; row.stock_qty = (frm.doc.quantity * data.percent) / 100; - row.qty = row.stock_qty / (row.conversion_factor ?? 1); + row.qty = row.stock_qty / (row.conversion_factor || 1); refresh_field("scrap_items"); }, __("Set Process Loss Item Quantity"), From 7ed8c8c11f9c62ab95c45a076ba48ccad80369d1 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Jun 2021 15:48:28 +0530 Subject: [PATCH 09/48] fix: sider --- erpnext/manufacturing/doctype/bom/bom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 21693a33a07..00283de3afa 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -667,7 +667,7 @@ frappe.ui.form.on("BOM Scrap Item", { 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) + trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code); } }, }); From a21f76f2a1c49889e9e6a3a7b8c7b9bc6567d529 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 21 Jul 2021 20:08:20 +0200 Subject: [PATCH 10/48] feat: add voucher-specific data to datev export --- .../regional/germany/utils/datev/datev_csv.py | 8 + erpnext/regional/report/datev/datev.py | 196 ++++++++++++++++-- 2 files changed, 187 insertions(+), 17 deletions(-) diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index 122c15fd811..c5c2bc41f4d 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -33,6 +33,14 @@ def get_datev_csv(data, filters, csv_class): if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) + result['Beleginfo - Inhalt 6'] = pd.to_datetime(result['Beleginfo - Inhalt 6']) + result['Beleginfo - Inhalt 6'] = result['Beleginfo - Inhalt 6'].dt.strftime('%d%m%Y') + + result['Fälligkeit'] = pd.to_datetime(result['Fälligkeit']) + result['Fälligkeit'] = result['Fälligkeit'].dt.strftime('%d%m%y') + + result.sort_values(by='Belegdatum', inplace=True, kind='stable', ignore_index=True) + if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: result['Sprach-ID'] = 'de-DE' diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a5ca7eee5d4..5df24c2bb35 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -43,6 +43,12 @@ COLUMNS = [ "fieldtype": "Data", "width": 100 }, + { + "label": "BU-Schlüssel", + "fieldname": "BU-Schlüssel", + "fieldtype": "Data", + "width": 100 + }, { "label": "Belegdatum", "fieldname": "Belegdatum", @@ -114,6 +120,36 @@ COLUMNS = [ "fieldname": "Beleginfo - Inhalt 4", "fieldtype": "Data", "width": 150 + }, + { + "label": "Beleginfo - Art 5", + "fieldname": "Beleginfo - Art 5", + "fieldtype": "Data", + "width": 150 + }, + { + "label": "Beleginfo - Inhalt 5", + "fieldname": "Beleginfo - Inhalt 5", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Beleginfo - Art 6", + "fieldname": "Beleginfo - Art 6", + "fieldtype": "Data", + "width": 150 + }, + { + "label": "Beleginfo - Inhalt 6", + "fieldname": "Beleginfo - Inhalt 6", + "fieldtype": "Date", + "width": 100 + }, + { + "label": "Fälligkeit", + "fieldname": "Fälligkeit", + "fieldtype": "Date", + "width": 100 } ] @@ -161,6 +197,126 @@ def validate_fiscal_year(from_date, to_date, company): def get_transactions(filters, as_dict=1): + def run(params_method, filters): + extra_fields, extra_joins, extra_filters = params_method(filters) + return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict) + + type_map = { + # specific query methods for some voucher types + "Payment Entry": get_payment_entry_params, + "Sales Invoice": get_sales_invoice_params, + "Purchase Invoice": get_purchase_invoice_params + } + + only_voucher_type = filters.get("voucher_type") + transactions = [] + + for voucher_type, get_voucher_params in type_map.items(): + if only_voucher_type and only_voucher_type != voucher_type: + continue + + transactions.extend(run(params_method=get_voucher_params, filters=filters)) + + if not only_voucher_type or only_voucher_type not in type_map: + # generic query method for all other voucher types + filters["exclude_voucher_types"] = type_map.keys() + transactions.extend(run(params_method=get_generic_params, filters=filters)) + + if as_dict: + sort_by = lambda row: row["Belegdatum"] + else: + sort_by = lambda row: row[5] + + return sorted(transactions, key=sort_by) + + +def get_payment_entry_params(filters): + extra_fields = """ + , 'Zahlungsreferenz' as 'Beleginfo - Art 5' + , pe.reference_no as 'Beleginfo - Inhalt 5' + , 'Buchungstag' as 'Beleginfo - Art 6' + , pe.reference_date as 'Beleginfo - Inhalt 6' + , '' as 'Fälligkeit' + """ + + extra_joins = """ + LEFT JOIN `tabPayment Entry` pe + ON gl.voucher_no = pe.name + """ + + extra_filters = """ + AND gl.voucher_type = 'Payment Entry' + """ + + return extra_fields, extra_joins, extra_filters + + +def get_sales_invoice_params(filters): + extra_fields = """ + , '' as 'Beleginfo - Art 5' + , '' as 'Beleginfo - Inhalt 5' + , '' as 'Beleginfo - Art 6' + , '' as 'Beleginfo - Inhalt 6' + , si.due_date as 'Fälligkeit' + """ + + extra_joins = """ + LEFT JOIN `tabSales Invoice` si + ON gl.voucher_no = si.name + """ + + extra_filters = """ + AND gl.voucher_type = 'Sales Invoice' + """ + + return extra_fields, extra_joins, extra_filters + + +def get_purchase_invoice_params(filters): + extra_fields = """ + , 'Lieferanten-Rechnungsnummer' as 'Beleginfo - Art 5' + , pi.bill_no as 'Beleginfo - Inhalt 5' + , 'Lieferanten-Rechnungsdatum' as 'Beleginfo - Art 6' + , pi.bill_date as 'Beleginfo - Inhalt 6' + , pi.due_date as 'Fälligkeit' + """ + + extra_joins = """ + LEFT JOIN `tabPurchase Invoice` pi + ON gl.voucher_no = pi.name + """ + + extra_filters = """ + AND gl.voucher_type = 'Purchase Invoice' + """ + + return extra_fields, extra_joins, extra_filters + + +def get_generic_params(filters): + # produce empty fields so all rows will have the same length + extra_fields = """ + , '' as 'Beleginfo - Art 5' + , '' as 'Beleginfo - Inhalt 5' + , '' as 'Beleginfo - Art 6' + , '' as 'Beleginfo - Inhalt 6' + , '' as 'Fälligkeit' + """ + extra_joins = "" + + if filters.get("exclude_voucher_types"): + # exclude voucher types that are queried by a dedicated method + exclude = "({})".format(', '.join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))) + extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude) + + # if voucher type filter is set, allow only this type + if filters.get("voucher_type"): + extra_filters += " AND gl.voucher_type = %(voucher_type)s" + + return extra_fields, extra_joins, extra_filters + + +def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): """ Get a list of accounting entries. @@ -171,8 +327,7 @@ def get_transactions(filters, as_dict=1): filters -- dict of filters to be passed to the sql query as_dict -- return as list of dicts [0,1] """ - filter_by_voucher = 'AND gl.voucher_type = %(voucher_type)s' if filters.get('voucher_type') else '' - gl_entries = frappe.db.sql(""" + query = """ SELECT /* either debit or credit amount; always positive */ @@ -187,6 +342,9 @@ def get_transactions(filters, as_dict=1): /* against number or, if empty, party against number */ %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', + /* disable automatic VAT deduction */ + '40' as 'BU-Schlüssel', + gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', LEFT(gl.remarks, 60) as 'Buchungstext', @@ -199,30 +357,34 @@ def get_transactions(filters, as_dict=1): case gl.party_type when 'Customer' then 'Debitorennummer' when 'Supplier' then 'Kreditorennummer' else NULL end as 'Beleginfo - Art 4', par.debtor_creditor_number as 'Beleginfo - Inhalt 4' + {extra_fields} + FROM `tabGL Entry` gl /* Kontonummer */ - left join `tabAccount` acc - on gl.account = acc.name + LEFT JOIN `tabAccount` acc + ON gl.account = acc.name - left join `tabCustomer` cus - on gl.party_type = 'Customer' - and gl.party = cus.name + LEFT JOIN `tabParty Account` par + ON par.parent = gl.party + AND par.parenttype = gl.party_type + AND par.company = %(company)s - left join `tabSupplier` sup - on gl.party_type = 'Supplier' - and gl.party = sup.name - - left join `tabParty Account` par - on par.parent = gl.party - and par.parenttype = gl.party_type - and par.company = %(company)s + {extra_joins} WHERE gl.company = %(company)s AND DATE(gl.posting_date) >= %(from_date)s AND DATE(gl.posting_date) <= %(to_date)s - {} - ORDER BY 'Belegdatum', gl.voucher_no""".format(filter_by_voucher), filters, as_dict=as_dict) + + {extra_filters} + + ORDER BY 'Belegdatum', gl.voucher_no""".format( + extra_fields=extra_fields, + extra_joins=extra_joins, + extra_filters=extra_filters + ) + + gl_entries = frappe.db.sql(query, filters, as_dict=as_dict) return gl_entries From c6c2773e02a5b3f6d5a94079025bd642f8196127 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 3 Aug 2021 11:22:42 +0200 Subject: [PATCH 11/48] refactor: def instead of lambda --- erpnext/regional/report/datev/datev.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 5df24c2bb35..5dd71ea794a 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -200,6 +200,10 @@ def get_transactions(filters, as_dict=1): def run(params_method, filters): extra_fields, extra_joins, extra_filters = params_method(filters) return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict) + + def sort_by(row): + # "Belegdatum" is in the fifth column when list format is used + return row["Belegdatum" if as_dict else 5] type_map = { # specific query methods for some voucher types @@ -222,11 +226,6 @@ def get_transactions(filters, as_dict=1): filters["exclude_voucher_types"] = type_map.keys() transactions.extend(run(params_method=get_generic_params, filters=filters)) - if as_dict: - sort_by = lambda row: row["Belegdatum"] - else: - sort_by = lambda row: row[5] - return sorted(transactions, key=sort_by) From ce44e11caf590078ff2a0a31498fc1e2b519e7ce Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 9 Aug 2021 17:37:17 +0530 Subject: [PATCH 12/48] refactor: validation error message formatting --- erpnext/manufacturing/doctype/bom/bom.py | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 79f372eec10..4a817e8d9ae 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -678,29 +678,29 @@ class BOM(WebsiteGenerator): def validate_scrap_items(self): for item in self.scrap_items: + msg = "" if item.item_code == self.item and not item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + - _('should have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) + msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked') \ + .format(frappe.bold(item.item_code)) elif item.item_code != self.item and item.is_process_loss: - frappe.throw(_('Scrap/Loss Item:') + f' {frappe.bold(item.item_code)} ' + - _('should not have') + ' ' + frappe.bold(_('Is Process Loss')) + ' ' + ('checked')) + msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked') \ + .format(frappe.bold(item.item_code)) 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: - frappe.throw(_('Item:') + f' {frappe.bold(item.item_code)} ' + - _('with Stock UOM:') + f' {frappe.bold(item.stock_uom)} ' + - _('cannot be a Scrap/Loss Item')) + msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item") \ + .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) if item.is_process_loss and (item.stock_qty >= self.quantity): - frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + - _('should have') +' '+frappe.bold(_('Qty')) + ' ' + - _('less than finished goods') + ' ' + frappe.bold(_('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): - frappe.throw(_('Scrap/Loss Item:') + f' {item.item_code} ' + - _('should have') + ' ' + frappe.bold(_('Rate')) + - ' ' + _('set to 0 because') + ' ' + - frappe.bold(_('Is Process Loss')) + ' ' + _('is checked')) + 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")) def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" From 0992b2eeefea69df0d28961352d959873718e177 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 10 Aug 2021 12:23:19 +0530 Subject: [PATCH 13/48] test: check manufacture completion qty in se and wo --- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../doctype/work_order/test_work_order.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index d0bf677f643..8b9820c7bb5 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -230,7 +230,7 @@ class TestBOM(unittest.TestCase): def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() - if frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001") is None: + 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, 0.25, 0, 1 ) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bf1ccb71594..7f943d9cbb6 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -690,6 +690,64 @@ class TestWorkOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + def test_wo_completion_with_pl_bom(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items + from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item + + qty = fg_qty = 4 + 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) + + 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=fg_qty, is_process_loss=1 + ) + bom_doc.submit() + + wo = make_wo_order_test_record( + production_item=fg_item_non_whole.item_code, + bom_no=bom_no, + wip_warehouse=wip_warehouse, + qty=qty, + skip_transfer=1, + stock_uom=fg_item_non_whole.stock_uom, + ) + + se = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", 4) + ) + se.get("items")[0].s_warehouse = "Stores - _TC" + se.insert() + se.submit() + + se = frappe.get_doc( + make_stock_entry(wo.name, "Manufacture", 4) + ) + se.insert() + se.submit() + + # Testing stock entry values + items = se.get("items") + self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + + source_item, fg_item, pl_item = items + + total_pl_qty = scrap_qty * fg_qty + actual_fg_qty = fg_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"), actual_fg_qty) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` From 1c23544c42d96da39dfcc289abbbb87bf5db3763 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 10 Aug 2021 14:42:39 +0530 Subject: [PATCH 14/48] fix: wo tests, sider, account for pl in se validation --- .../doctype/work_order/test_work_order.py | 16 ++++++++-------- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 7f943d9cbb6..d6a20df0c89 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -694,7 +694,7 @@ class TestWorkOrder(unittest.TestCase): from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item - qty = fg_qty = 4 + qty = 4 scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" @@ -707,7 +707,7 @@ class TestWorkOrder(unittest.TestCase): 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=fg_qty, is_process_loss=1 + scrap_rate=0, fg_qty=1, is_process_loss=1 ) bom_doc.submit() @@ -721,32 +721,32 @@ class TestWorkOrder(unittest.TestCase): ) se = frappe.get_doc( - make_stock_entry(wo.name, "Material Transfer for Manufacture", 4) + make_stock_entry(wo.name, "Material Transfer for Manufacture", qty) ) se.get("items")[0].s_warehouse = "Stores - _TC" se.insert() se.submit() se = frappe.get_doc( - make_stock_entry(wo.name, "Manufacture", 4) + make_stock_entry(wo.name, "Manufacture", qty) ) se.insert() se.submit() # 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), 4, "There should be 3 items including process loss.") source_item, fg_item, pl_item = items - total_pl_qty = scrap_qty * fg_qty - actual_fg_qty = fg_qty - total_pl_qty + 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"), actual_fg_qty) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), actual_fg_qty) def get_scrap_item_details(bom_no): scrap_items = {} diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 480700a273b..ce9b012cd67 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -270,7 +270,7 @@ class StockEntry(StockController): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item: + if d.is_finished_item or d.is_process_loss: item_wise_qty.setdefault(d.item_code, []).append(d.qty) for item_code, qty_list in iteritems(item_wise_qty): From f8a47525e1f7b5f79e1c77f87918a8d2f4cf75ca Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 17 Aug 2021 16:03:04 +0530 Subject: [PATCH 15/48] fix: reword error messages, fix test values --- erpnext/manufacturing/doctype/bom/bom.py | 10 +++++----- .../doctype/work_order/test_work_order.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 4a817e8d9ae..02db2607485 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -680,23 +680,23 @@ class BOM(WebsiteGenerator): 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') \ + 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') \ + 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", 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") \ + 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 item.is_process_loss and (item.stock_qty >= self.quantity): - msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods 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") \ + 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: diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d6a20df0c89..0569092f883 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -735,7 +735,7 @@ class TestWorkOrder(unittest.TestCase): # Testing stock entry values items = se.get("items") - self.assertEqual(len(items), 4, "There should be 3 items including process loss.") + self.assertEqual(len(items), 3, "There should be 3 items including process loss.") source_item, fg_item, pl_item = items @@ -746,7 +746,7 @@ class TestWorkOrder(unittest.TestCase): self.assertEqual(fg_item.qty, actual_fg_qty) # Testing Work Order values - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), actual_fg_qty) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) def get_scrap_item_details(bom_no): scrap_items = {} From a14b93d0d892d0c33dd49372124c9f8fb06e6ea0 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 16:11:29 +0530 Subject: [PATCH 16/48] feat: add procss_loss_qty field in work order --- .../doctype/work_order/test_work_order.py | 9 +++++- .../doctype/work_order/work_order.json | 30 +++++++++++-------- .../doctype/work_order/work_order.py | 16 ++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0569092f883..a00520f6a1b 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -746,7 +746,14 @@ class TestWorkOrder(unittest.TestCase): 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, "produced_qty"), + qty + ) + self.assertEqual( + frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), + actual_fg_qty + ) def get_scrap_item_details(bom_no): scrap_items = {} diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 3b56854aaf3..913fc85af61 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -19,6 +19,7 @@ "qty", "material_transferred_for_manufacturing", "produced_qty", + "process_loss_qty", "sales_order", "project", "serial_no_and_batch_for_finished_good_section", @@ -64,16 +65,12 @@ "description", "stock_uom", "column_break2", - "references_section", "material_request", "material_request_item", "sales_order_item", - "column_break_61", "production_plan", "production_plan_item", "production_plan_sub_assembly_item", - "parent_work_order", - "bom_level", "product_bundle_item", "amended_from" ], @@ -553,20 +550,29 @@ "read_only": 1 }, { - "fieldname": "production_plan_sub_assembly_item", - "fieldtype": "Data", - "label": "Production Plan Sub-assembly Item", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - } + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "label": "Production Plan Sub-assembly Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval: doc.process_loss_qty", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + } ], "icon": "fa fa-cogs", "idx": 1, "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-06-28 16:19:14.902699", + "modified": "2021-08-24 15:14:03.844937", "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 0a8e5329c15..6e841f40287 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -214,6 +214,7 @@ class WorkOrder(Document): self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError) self.db_set(fieldname, qty) + self.set_process_loss_qty() from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item @@ -223,6 +224,21 @@ 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 docstatus=1 + ) + """, (self.name, ))[0][0]) + if process_loss_qty is not None: + self.db_set('process_loss_qty', process_loss_qty) + def update_production_plan_status(self): production_plan = frappe.get_doc('Production Plan', self.production_plan) produced_qty = 0 From 795efcd017c0f123881cd0ab9eae761675264cf6 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 18:23:49 +0530 Subject: [PATCH 17/48] feat: process loss report, fix set pl query condition --- .../doctype/work_order/work_order.py | 1 + .../report/process_loss_report/__init__.py | 0 .../process_loss_report.js | 37 +++++ .../process_loss_report.json | 29 ++++ .../process_loss_report.py | 132 ++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 erpnext/stock/report/process_loss_report/__init__.py create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.js create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.json create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.py diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 6e841f40287..ebb1b09e3b2 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -233,6 +233,7 @@ class WorkOrder(Document): SELECT name FROM `tabStock Entry` WHERE work_order=%s + AND purpose='Manufacture' AND docstatus=1 ) """, (self.name, ))[0][0]) diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/stock/report/process_loss_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/stock/report/process_loss_report/process_loss_report.js new file mode 100644 index 00000000000..078b9e11ce9 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.js @@ -0,0 +1,37 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Process Loss Report"] = { + filters: [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), + }, + { + label: __("Item"), + fieldname: "item", + fieldtype: "Link", + options: "Item", + mandatory: false, + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ] +}; diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.json b/erpnext/stock/report/process_loss_report/process_loss_report.json new file mode 100644 index 00000000000..afe4aff7f1c --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-08-24 16:38:15.233395", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-08-24 16:38:15.233395", + "modified_by": "Administrator", + "module": "Stock", + "name": "Process Loss Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Process Loss Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/stock/report/process_loss_report/process_loss_report.py new file mode 100644 index 00000000000..be0f0151d40 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.py @@ -0,0 +1,132 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from typing import Dict, List, Tuple + +Filters = frappe._dict +Row = frappe._dict +Data = List[Row] +Columns = List[Dict[str, str]] +QueryArgs = Dict[str, str] + +def execute(filters: Filters) -> Tuple[Columns, Data]: + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters: Filters) -> Data: + query_args = get_query_args(filters) + data = run_query(query_args) + update_data_with_total_pl_value(data) + return data + +def get_columns() -> Columns: + return [ + { + 'label': 'Work Order', + 'fieldname': 'name', + 'fieldtype': 'Link', + 'options': 'Work Order', + 'width': '200' + }, + { + 'label': 'Item', + 'fieldname': 'production_item', + 'fieldtype': 'Link', + 'options': 'Item', + 'width': '100' + }, + { + 'label': 'Status', + 'fieldname': 'status', + 'fieldtype': 'Data', + 'width': '100' + }, + { + 'label': 'Qty To Manufacture', + 'fieldname': 'qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Manufactured Qty', + 'fieldname': 'produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Process Loss Qty', + 'fieldname': 'process_loss_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Actual Manufactured Qty', + 'fieldname': 'actual_produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Total FG Value', + 'fieldname': 'total_fg_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Total Raw Material Value', + 'fieldname': 'total_rm_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Total Process Loss Value', + 'fieldname': 'total_pl_value', + 'fieldtype': 'Float', + 'width': '150' + }, + ] + +def get_query_args(filters: Filters) -> QueryArgs: + query_args = {} + query_args.update(filters) + query_args.update( + get_filter_conditions(filters) + ) + return query_args + +def run_query(query_args: QueryArgs) -> Data: + return frappe.db.sql(""" + SELECT + wo.name, wo.status, wo.production_item, wo.qty, + wo.produced_qty, wo.process_loss_qty, + (wo.produced_qty - wo.process_loss_qty) as actual_produced_qty, + sum(se.total_incoming_value) as total_fg_value, + sum(se.total_outgoing_value) as total_rm_value + FROM + `tabWork Order` wo INNER JOIN `tabStock Entry` se + ON wo.name=se.work_order + WHERE + process_loss_qty > 0 + AND wo.company = %(company)s + AND se.docstatus = 1 + AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s + %(item_filter)s + GROUP BY + se.work_order + """, query_args, as_dict=1) + +def update_data_with_total_pl_value(data: Data) -> None: + for row in data: + value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty'] + row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg + +def get_filter_conditions(filters: Filters) -> QueryArgs: + filter_conditions = dict(item_filter="") + if "item" in filters: + production_item = filters.get("item") + filter_conditions.update( + {"item_filter": f"wo.production_item='{production_item}'"} + ) + return filter_conditions + From 9e96861f1575a91a8b425fd318a3b6e2eca8c810 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 20:18:53 +0530 Subject: [PATCH 18/48] fix: correct value in test --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index a00520f6a1b..3a334a530cd 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -752,7 +752,7 @@ class TestWorkOrder(unittest.TestCase): ) self.assertEqual( frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), - actual_fg_qty + total_pl_qty ) def get_scrap_item_details(bom_no): From 4eb7c2a011f3abbe547c2a32978e7af6e1fd6d33 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 25 Aug 2021 16:54:45 +0530 Subject: [PATCH 19/48] fix: TDS calculation on net total (#27058) --- .../tax_withholding_category.py | 9 +++--- .../test_tax_withholding_category.py | 32 ++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 1536a237dec..0cb872c4b81 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -240,14 +240,15 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): tds_amount = 0 invoice_filters = { - 'name': ('in', vouchers), - 'docstatus': 1 + 'name': ('in', vouchers), + 'docstatus': 1, + 'apply_tds': 1 } field = 'sum(net_total)' - if not cint(tax_details.consider_party_ledger_amount): - invoice_filters.update({'apply_tds': 1}) + if cint(tax_details.consider_party_ledger_amount): + invoice_filters.pop('apply_tds', None) field = 'sum(grand_total)' supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 1c687e5cb15..0f921db678d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -145,6 +145,36 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_tds_calculation_on_net_total(self): + frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS") + invoices = [] + + pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True) + pi.append('taxes', { + "category": "Total", + "charge_type": "Actual", + "account_head": '_Test Account VAT - _TC', + "cost_center": 'Main - _TC', + "tax_amount": 1000, + "description": "Test", + "add_deduct_tax": "Add" + + }) + pi.save() + pi.submit() + invoices.append(pi) + + # Second Invoice will apply TDS checked + pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000) + pi1.submit() + invoices.append(pi1) + + self.assertEqual(pi1.taxes[0].tax_amount, 4000) + + #delete invoices to avoid clashing + for d in invoices: + d.cancel() + def cancel_invoices(): purchase_invoices = frappe.get_all("Purchase Invoice", { 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], @@ -220,7 +250,7 @@ def create_sales_invoice(**args): def create_records(): # create a new suppliers - for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']: + for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']: if frappe.db.exists('Supplier', name): continue From 7ac4916191c3b038ebd67f18c2f96dfc4f4ba112 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 16:59:03 +0530 Subject: [PATCH 20/48] feat: unreconcile on cancellation of bank transaction (#27109) (#27137) --- .../bank_transaction/bank_transaction.py | 31 +++++++++++++------ .../bank_transaction/bank_transaction_list.js | 6 ++-- .../bank_transaction/test_bank_transaction.py | 9 +++++- erpnext/controllers/status_updater.py | 3 +- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 31cfb2da1da..0544a469d60 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -21,6 +21,10 @@ class BankTransaction(StatusUpdater): self.update_allocations() self.clear_linked_payment_entries() self.set_status(update=True) + + def on_cancel(self): + self.clear_linked_payment_entries(for_cancel=True) + self.set_status(update=True) def update_allocations(self): if self.payment_entries: @@ -41,21 +45,30 @@ class BankTransaction(StatusUpdater): frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") self.reload() - - def clear_linked_payment_entries(self): + + def clear_linked_payment_entries(self, for_cancel=False): for payment_entry in self.payment_entries: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: - self.clear_simple_entry(payment_entry) + self.clear_simple_entry(payment_entry, for_cancel=for_cancel) elif payment_entry.payment_document == "Sales Invoice": - self.clear_sales_invoice(payment_entry) + self.clear_sales_invoice(payment_entry, for_cancel=for_cancel) - def clear_simple_entry(self, payment_entry): - frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date) + def clear_simple_entry(self, payment_entry, for_cancel=False): + clearance_date = self.date if not for_cancel else None + frappe.db.set_value( + payment_entry.payment_document, payment_entry.payment_entry, + "clearance_date", clearance_date) - def clear_sales_invoice(self, payment_entry): - frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.payment_document, - parent=payment_entry.payment_entry), "clearance_date", self.date) + def clear_sales_invoice(self, payment_entry, for_cancel=False): + clearance_date = self.date if not for_cancel else None + frappe.db.set_value( + "Sales Invoice Payment", + dict( + parenttype=payment_entry.payment_document, + parent=payment_entry.payment_entry + ), + "clearance_date", clearance_date) def get_total_allocated_amount(payment_entry): return frappe.db.sql(""" diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js index bff41d5539b..2585ee9c923 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js @@ -4,10 +4,12 @@ frappe.listview_settings['Bank Transaction'] = { add_fields: ["unallocated_amount"], get_indicator: function(doc) { - if(flt(doc.unallocated_amount)>0) { - return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; + if(doc.docstatus == 2) { + return [__("Cancelled"), "red", "docstatus,=,2"]; } else if(flt(doc.unallocated_amount)<=0) { return [__("Reconciled"), "green", "unallocated_amount,=,0"]; + } else if(flt(doc.unallocated_amount)>0) { + return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; } } }; diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index ce149f96e6f..439d4891194 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -25,7 +25,8 @@ class TestBankTransaction(unittest.TestCase): def tearDownClass(cls): for bt in frappe.get_all("Bank Transaction"): doc = frappe.get_doc("Bank Transaction", bt.name) - doc.cancel() + if doc.docstatus == 1: + doc.cancel() doc.delete() # Delete directly in DB to avoid validation errors for countries not allowing deletion @@ -57,6 +58,12 @@ class TestBankTransaction(unittest.TestCase): clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") self.assertTrue(clearance_date is not None) + bank_transaction.reload() + bank_transaction.cancel() + + clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") + self.assertFalse(clearance_date) + # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index b1f89b08d79..7b24e50b143 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -86,7 +86,8 @@ status_map = { ], "Bank Transaction": [ ["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"], - ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"] + ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"], + ["Cancelled", "eval:self.docstatus == 2"] ], "POS Opening Entry": [ ["Draft", None], From d97a87e28de1aa4b6b9465270ac4347dbf69f5aa Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 17:12:04 +0530 Subject: [PATCH 21/48] fix(healthcare): Made payment fields mandatory for new appointments (#27135) * fix(healthcare): Made payment fields mandatory for new appointments (#26608) * fix(healthcare): Made payment fields mandatory for new appointments * fix: sider issues * fix: Fix failing test * fix: Patient appointment invoicing Co-authored-by: Rucha Mahabal Co-authored-by: Syed Mujeer Hashmi (cherry picked from commit a65498dc610287640e76dc8a646e9ada0c29a44a) # Conflicts: # erpnext/healthcare/doctype/patient_appointment/patient_appointment.json # erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py * chore: Fix merge conflicts * chore: Fix failing tests Co-authored-by: Chillar Anand --- .../doctype/fee_validity/test_fee_validity.py | 4 +-- .../patient_appointment.js | 7 ++++++ .../patient_appointment.json | 3 ++- .../patient_appointment.py | 8 +----- .../test_patient_appointment.py | 25 ++++++++++++++++++- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 54f388b370b..29b4c5c9b98 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -29,10 +29,10 @@ class TestFeeValidity(unittest.TestCase): healthcare_settings.save(ignore_permissions=True) patient, practitioner = create_healthcare_docs() - # appointment should not be invoiced. Check Fee Validity created for new patient + # For first appointment, invoice is generated appointment = create_appointment(patient, practitioner, nowdate()) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") - self.assertEqual(invoiced, 0) + self.assertEqual(invoiced, 1) # appointment should not be invoiced as it is within fee validity appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4)) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 8923e014452..49847d5bc8a 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -241,6 +241,13 @@ frappe.ui.form.on('Patient Appointment', { frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('paid_amount', 0); frm.toggle_reqd('billing_item', 0); + } else if (data.message) { + frm.toggle_display('mode_of_payment', 1); + frm.toggle_display('paid_amount', 1); + frm.toggle_display('billing_item', 1); + frm.toggle_reqd('mode_of_payment', 1); + frm.toggle_reqd('paid_amount', 1); + frm.toggle_reqd('billing_item', 1); } else { // if automated appointment invoicing is disabled, hide fields frm.toggle_display('mode_of_payment', data.message ? 1 : 0); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 7654e0d249f..0267e65a212 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -134,6 +134,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.practitioner;", "fieldname": "section_break_12", "fieldtype": "Section Break", "label": "Appointment Details" @@ -400,4 +401,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 7d7e078647f..10f2d537891 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -163,8 +163,6 @@ def check_payment_fields_reqd(patient): fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'}) if fee_validity: return {'fee_validity': fee_validity} - if check_is_new_patient(patient): - return False return True return False @@ -179,8 +177,6 @@ def invoice_appointment(appointment_doc): elif not fee_validity: if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}): return - if check_is_new_patient(appointment_doc.patient, appointment_doc.name): - return else: fee_validity = None @@ -224,9 +220,7 @@ def check_is_new_patient(patient, name=None): filters['name'] = ('!=', name) has_previous_appointment = frappe.db.exists('Patient Appointment', filters) - if has_previous_appointment: - return False - return True + return not has_previous_appointment def get_appointment_item(appointment_doc, item): diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 18dc5bd5cea..c0fe5f08854 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -4,11 +4,12 @@ from __future__ import unicode_literals import unittest import frappe -from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter +from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter, check_payment_fields_reqd, check_is_new_patient from frappe.utils import nowdate, add_days, now_datetime from frappe.utils.make_random import get_random from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile + class TestPatientAppointment(unittest.TestCase): def setUp(self): frappe.db.sql("""delete from `tabPatient Appointment`""") @@ -179,6 +180,28 @@ class TestPatientAppointment(unittest.TestCase): mark_invoiced_inpatient_occupancy(ip_record1) discharge_patient(ip_record1, now_datetime()) + def test_payment_should_be_mandatory_for_new_patient_appointment(self): + frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + frappe.db.set_value('Healthcare Settings', None, 'max_visits', 3) + frappe.db.set_value('Healthcare Settings', None, 'valid_days', 30) + + patient = create_patient() + assert check_is_new_patient(patient) + payment_required = check_payment_fields_reqd(patient) + assert payment_required is True + + def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): + patient, practitioner = create_healthcare_docs() + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + invoice_count = frappe.db.count('Sales Invoice') + + assert check_is_new_patient(patient) + create_appointment(patient, practitioner, nowdate()) + new_invoice_count = frappe.db.count('Sales Invoice') + + assert new_invoice_count == invoice_count + 1 + def test_overlap_appointment(self): from erpnext.healthcare.doctype.patient_appointment.patient_appointment import OverlapError patient, practitioner = create_healthcare_docs(id=1) From 1be810479c7288a9e4bdaaf5db6568b50eddfb08 Mon Sep 17 00:00:00 2001 From: Alan <2.alan.tom@gmail.com> Date: Wed, 25 Aug 2021 17:45:55 +0530 Subject: [PATCH 22/48] refactor: update stock module onboarding (#25745) * refactor: update stock onboarding * refactor: add form tour for stock module onboarding * refactor: move trailing whitespace out of translate func * refactor: sider/semgrep * refactor: remove DN, PR; change wording, add/remove steps in tour * refactor: add watch video step for stock opening balance * refactor: reorder steps according to stock settings refactor * refactor: fix typo, remove target warehouse cause SE Type dependency * fix: semgrep, remove trailing and leading whitespaces * refactor: reduce steps, reword cards * fix: minor changes - remove Is Group from warehouse - change stock entry type - link to stock entry type - add posting date to stock reco - change report to Stock Projected Qty - highlight quality inspection action - remove allow neg highlight * refactor: use Form Tour doc instead of controller form tour note - keeping controller form tours as a fallback, new form tours seem to work only for Stock Settings * fix: rename form tours to doctype names, remove tours from js controllers * fix: re-order tour to circumvent glitchy save highlight --- erpnext/hooks.py | 1 + .../doctype/delivery_note/delivery_note.js | 20 +++++ erpnext/stock/doctype/item/item.js | 2 +- .../stock/doctype/stock_entry/stock_entry.js | 1 + .../stock_reconciliation.js | 1 + .../doctype/stock_settings/stock_settings.js | 33 ------- erpnext/stock/doctype/warehouse/warehouse.js | 1 + .../form_tour/stock_entry/stock_entry.json | 56 ++++++++++++ .../stock_reconciliation.json | 55 ++++++++++++ .../stock_settings/stock_settings.json | 89 +++++++++++++++++++ .../stock/form_tour/warehouse/warehouse.json | 54 +++++++++++ .../stock/module_onboarding/stock/stock.json | 16 ++-- .../create_a_purchase_receipt.json | 19 ---- .../create_a_stock_entry.json | 8 +- .../create_a_supplier/create_a_supplier.json | 7 +- .../create_a_warehouse.json | 21 +++++ .../create_an_item/create_an_item.json | 22 +++++ .../introduction_to_stock_entry.json | 5 +- .../setup_your_warehouse.json | 4 +- .../stock_opening_balance.json | 22 +++++ .../stock_settings/stock_settings.json | 8 +- .../view_stock_projected_qty.json | 24 +++++ .../view_warehouses/view_warehouses.json | 20 +++++ 23 files changed, 412 insertions(+), 77 deletions(-) create mode 100644 erpnext/stock/form_tour/stock_entry/stock_entry.json create mode 100644 erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json create mode 100644 erpnext/stock/form_tour/stock_settings/stock_settings.json create mode 100644 erpnext/stock/form_tour/warehouse/warehouse.json delete mode 100644 erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json create mode 100644 erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json create mode 100644 erpnext/stock/onboarding_step/create_an_item/create_an_item.json create mode 100644 erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json create mode 100644 erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json create mode 100644 erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c21b240a019..748bd088077 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -444,6 +444,7 @@ regional_overrides = { 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', + 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 74cb3fcb1f0..8632c9c1085 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -356,3 +356,23 @@ erpnext.stock.delivery_note.set_print_hide = function(doc, cdt, cdn){ dn_fields['taxes'].print_hide = 0; } } + + +frappe.tour['Delivery Note'] = [ + { + fieldname: "customer", + title: __("Customer"), + description: __("This field is used to set the 'Customer'.") + }, + { + fieldname: "items", + title: __("Items"), + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " + + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") + }, + { + fieldname: "set_posting_time", + title: __("Edit Posting Date and Time"), + description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.") + } +] diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 6c5ef8b4f03..cbd0231e66a 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -792,4 +792,4 @@ frappe.ui.form.on("UOM Conversion Detail", { }); } } -}) +}); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 67083930272..efbc12ce841 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1101,3 +1101,4 @@ function check_should_not_attach_bom_items(bom_no) { } $.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm})); + diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 349e59f31d1..99694690bbc 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -288,3 +288,4 @@ erpnext.stock.StockReconciliation = erpnext.stock.StockController.extend({ }); cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); + diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 48624e0f25e..6167becdaac 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -16,36 +16,3 @@ frappe.ui.form.on('Stock Settings', { } }); -frappe.tour['Stock Settings'] = [ - { - fieldname: "item_naming_by", - title: __("Item Naming By"), - description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."), - }, - { - fieldname: "default_warehouse", - title: __("Default Warehouse"), - description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.") - }, - { - fieldname: "allow_negative_stock", - title: __("Allow Negative Stock"), - description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.") - - }, - { - fieldname: "valuation_method", - title: __("Valuation Method"), - description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "here" + __(" to know more about them.") - }, - { - fieldname: "show_barcode_field", - title: __("Show Barcode Field"), - description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.") - }, - { - fieldname: "automatically_set_serial_nos_based_on_fifo", - title: __("Automatically Set Serial Nos based on FIFO"), - description: __("Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.") - } -]; diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 9243e1ed84f..4e1679c4116 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -86,3 +86,4 @@ function convert_to_group_or_ledger(frm){ }) } + diff --git a/erpnext/stock/form_tour/stock_entry/stock_entry.json b/erpnext/stock/form_tour/stock_entry/stock_entry.json new file mode 100644 index 00000000000..6363c6ad4dd --- /dev/null +++ b/erpnext/stock/form_tour/stock_entry/stock_entry.json @@ -0,0 +1,56 @@ +{ + "creation": "2021-08-24 14:44:22.292652", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-25 16:31:31.441194", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Entry", + "owner": "Administrator", + "reference_doctype": "Stock Entry", + "save_on_complete": 1, + "steps": [ + { + "description": "Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select Material Receipt.", + "field": "", + "fieldname": "stock_entry_type", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Stock Entry Type", + "next_step_condition": "eval: doc.stock_entry_type === \"Material Receipt\"", + "parent_field": "", + "position": "Top", + "title": "Stock Entry Type" + }, + { + "description": "Select a target warehouse where the stock will be received.", + "field": "", + "fieldname": "to_warehouse", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Default Target Warehouse", + "next_step_condition": "eval: doc.to_warehouse", + "parent_field": "", + "position": "Top", + "title": "Default Target Warehouse" + }, + { + "description": "Select an item and entry quantity to be delivered.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Items", + "next_step_condition": "eval: doc.items[0]?.item_code", + "parent_field": "", + "position": "Top", + "title": "Items" + } + ], + "title": "Stock Entry" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json new file mode 100644 index 00000000000..5b7fd72c082 --- /dev/null +++ b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json @@ -0,0 +1,55 @@ +{ + "creation": "2021-08-24 14:44:46.770952", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-25 16:26:11.718664", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Reconciliation", + "owner": "Administrator", + "reference_doctype": "Stock Reconciliation", + "save_on_complete": 1, + "steps": [ + { + "description": "Set Purpose to Opening Stock to set the stock opening balance.", + "field": "", + "fieldname": "purpose", + "fieldtype": "Select", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Purpose", + "next_step_condition": "eval: doc.purpose === \"Opening Stock\"", + "parent_field": "", + "position": "Top", + "title": "Purpose" + }, + { + "description": "Select the items for which the opening stock has to be set.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Items", + "next_step_condition": "eval: doc.items[0]?.item_code", + "parent_field": "", + "position": "Top", + "title": "Items" + }, + { + "description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.", + "field": "", + "fieldname": "posting_date", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Posting Date", + "parent_field": "", + "position": "Bottom", + "title": "Posting Date" + } + ], + "title": "Stock Reconciliation" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_settings/stock_settings.json b/erpnext/stock/form_tour/stock_settings/stock_settings.json new file mode 100644 index 00000000000..3d164e33b3b --- /dev/null +++ b/erpnext/stock/form_tour/stock_settings/stock_settings.json @@ -0,0 +1,89 @@ +{ + "creation": "2021-08-20 15:20:59.336585", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-25 16:19:37.699528", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Settings", + "owner": "Administrator", + "reference_doctype": "Stock Settings", + "save_on_complete": 1, + "steps": [ + { + "description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.", + "field": "", + "fieldname": "item_naming_by", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Naming By", + "parent_field": "", + "position": "Bottom", + "title": "Item Naming By" + }, + { + "description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.", + "field": "", + "fieldname": "default_warehouse", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Warehouse", + "parent_field": "", + "position": "Bottom", + "title": "Default Warehouse" + }, + { + "description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.", + "field": "", + "fieldname": "action_if_quality_inspection_is_not_submitted", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Action If Quality Inspection Is Not Submitted", + "parent_field": "", + "position": "Bottom", + "title": "Action if Quality Inspection Is Not Submitted" + }, + { + "description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.", + "field": "", + "fieldname": "automatically_set_serial_nos_based_on_fifo", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Automatically Set Serial Nos Based on FIFO", + "parent_field": "", + "position": "Bottom", + "title": "Automatically Set Serial Nos based on FIFO" + }, + { + "description": "Show 'Scan Barcode' field above every child table to insert Items with ease.", + "field": "", + "fieldname": "show_barcode_field", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Show Barcode Field in Stock Transactions", + "parent_field": "", + "position": "Bottom", + "title": "Show Barcode Field" + }, + { + "description": "Choose between FIFO and Moving Average Valuation Methods. Click here to know more about them.", + "field": "", + "fieldname": "valuation_method", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Valuation Method", + "parent_field": "", + "position": "Bottom", + "title": "Default Valuation Method" + } + ], + "title": "Stock Settings" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/warehouse/warehouse.json b/erpnext/stock/form_tour/warehouse/warehouse.json new file mode 100644 index 00000000000..23ff2aebbaa --- /dev/null +++ b/erpnext/stock/form_tour/warehouse/warehouse.json @@ -0,0 +1,54 @@ +{ + "creation": "2021-08-24 14:43:44.465237", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-24 14:50:31.988256", + "modified_by": "Administrator", + "module": "Stock", + "name": "Warehouse", + "owner": "Administrator", + "reference_doctype": "Warehouse", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a name for the warehouse. This should reflect its location or purpose.", + "field": "", + "fieldname": "warehouse_name", + "fieldtype": "Data", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Warehouse Name", + "next_step_condition": "eval: doc.warehouse_name", + "parent_field": "", + "position": "Bottom", + "title": "Warehouse Name" + }, + { + "description": "Select a warehouse type to categorize the warehouse into a sub-group.", + "field": "", + "fieldname": "warehouse_type", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Warehouse Type", + "parent_field": "", + "position": "Top", + "title": "Warehouse Type" + }, + { + "description": "Select an account to set a default account for all transactions with this warehouse.", + "field": "", + "fieldname": "account", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Account", + "parent_field": "", + "position": "Top", + "title": "Account" + } + ], + "title": "Warehouse" +} \ No newline at end of file diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json index 847464822b4..c246747a5b3 100644 --- a/erpnext/stock/module_onboarding/stock/stock.json +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -19,32 +19,26 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "idx": 0, "is_complete": 0, - "modified": "2020-10-14 14:54:42.741971", + "modified": "2021-08-20 14:38:55.570067", "modified_by": "Administrator", "module": "Stock", "name": "Stock", "owner": "Administrator", "steps": [ { - "step": "Setup your Warehouse" + "step": "Stock Settings" }, { - "step": "Create a Product" - }, - { - "step": "Create a Supplier" - }, - { - "step": "Introduction to Stock Entry" + "step": "Create a Warehouse" }, { "step": "Create a Stock Entry" }, { - "step": "Create a Purchase Receipt" + "step": "Stock Opening Balance" }, { - "step": "Stock Settings" + "step": "View Stock Projected Qty" } ], "subtitle": "Inventory, Warehouses, Analysis, and more.", diff --git a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json deleted file mode 100644 index 9012493f57e..00000000000 --- a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "action": "Create Entry", - "creation": "2020-05-19 18:59:13.266713", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_mandatory": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2020-10-14 14:53:25.618434", - "modified_by": "Administrator", - "name": "Create a Purchase Receipt", - "owner": "Administrator", - "reference_document": "Purchase Receipt", - "show_full_form": 1, - "title": "Create a Purchase Receipt", - "validate_action": 1 -} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json index 09902b8844e..3cb522c893d 100644 --- a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json +++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json @@ -1,19 +1,21 @@ { "action": "Create Entry", + "action_label": "Create a Material Transfer Entry", "creation": "2020-05-15 03:20:16.277043", + "description": "# Manage Stock Movements\nStock entry allows you to register the movement of stock for various purposes like transfer, received, issues, repacked, etc. To address issues related to theft and pilferages, you can always ensure that the movement of goods happens against a document reference Stock Entry in ERPNext.\n\nLet\u2019s get a quick walk-through on the various scenarios covered in Stock Entry by watching [*this video*](https://www.youtube.com/watch?v=Njt107hlY3I).", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.105905", + "modified": "2021-06-18 13:57:11.434063", "modified_by": "Administrator", "name": "Create a Stock Entry", "owner": "Administrator", "reference_document": "Stock Entry", + "show_form_tour": 1, "show_full_form": 1, - "title": "Create a Stock Entry", + "title": "Manage Stock Movements", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json index ef61fa3b2e2..49efe578a29 100644 --- a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json @@ -1,18 +1,19 @@ { - "action": "Create Entry", + "action": "Show Form Tour", "creation": "2020-05-14 22:09:10.043554", + "description": "# Create a Supplier\nIn this step we will create a **Supplier**. If you have already created a **Supplier** you can skip this step.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.120455", + "modified": "2021-05-17 16:37:37.697077", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", "reference_document": "Supplier", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Supplier", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json new file mode 100644 index 00000000000..22c88bf10ea --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create your first warehouse ", + "creation": "2021-05-17 16:13:19.297789", + "description": "# Setup a Warehouse\nThe warehouse can be your location/godown/store where you maintain the item's inventory, and receive/deliver them to various parties.\n\nIn ERPNext, you can maintain a Warehouse in the tree structure, so that location and sub-location of an item can be tracked. Also, you can link a Warehouse to a specific Accounting ledger, where the real-time stock value of that warehouse\u2019s item will be reflected.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-18 12:23:36.675572", + "modified_by": "Administrator", + "name": "Create a Warehouse", + "owner": "Administrator", + "reference_document": "Warehouse", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Setup a Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_an_item/create_an_item.json b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json new file mode 100644 index 00000000000..016cbd566d5 --- /dev/null +++ b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "", + "creation": "2021-05-17 13:47:18.515052", + "description": "# Create an Item\nThe Stock module deals with the movement of items.\n\nIn this step we will create an [**Item**](https://docs.erpnext.com/docs/user/manual/en/stock/item).", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 16:15:20.695028", + "modified_by": "Administrator", + "name": "Create an Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create an Item", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json index 212e5055eda..384950e8b99 100644 --- a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json +++ b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json @@ -1,17 +1,18 @@ { "action": "Watch Video", "creation": "2020-05-15 02:47:17.958806", + "description": "# Introduction to Stock Entry\nThis video will give a quick introduction to [**Stock Entry**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-entry).", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.075177", + "modified": "2021-05-18 15:13:43.306064", "modified_by": "Administrator", "name": "Introduction to Stock Entry", "owner": "Administrator", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Stock Entry", "validate_action": 1, diff --git a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 75940ed2a6c..5d33a649100 100644 --- a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:25.538900", + "modified": "2021-05-17 13:53:06.936579", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", + "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json new file mode 100644 index 00000000000..48fd1fddee0 --- /dev/null +++ b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "Let\u2019s create a stock opening entry", + "creation": "2021-05-17 16:13:47.511883", + "description": "# Update Stock Opening Balance\nIt\u2019s an entry to update the stock balance of an item, in a warehouse, on a date and time you are going live on ERPNext.\n\nOnce opening stocks are updated, you can create transactions like manufacturing and stock deliveries, where this opening stock will be consumed.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-06-18 13:59:36.021097", + "modified_by": "Administrator", + "name": "Stock Opening Balance", + "owner": "Administrator", + "reference_document": "Stock Reconciliation", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Update Stock Opening Balance", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=nlHX0ZZ84Lw" +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json index ae34afa695f..2cf90e806cd 100644 --- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json +++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json @@ -1,19 +1,21 @@ { "action": "Show Form Tour", + "action_label": "Take a walk through Stock Settings", "creation": "2020-05-15 02:53:57.209967", + "description": "# Review Stock Settings\n\nIn ERPNext, the Stock module\u2019s features are configurable as per your business needs. Stock Settings is the place where you can set your preferences for:\n- Default values for Item and Pricing\n- Default valuation method for inventory valuation\n- Set preference for serialization and batching of item\n- Set tolerance for over-receipt and delivery of items", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.092504", + "modified": "2021-08-18 12:06:51.139387", "modified_by": "Administrator", "name": "Stock Settings", "owner": "Administrator", "reference_document": "Stock Settings", + "show_form_tour": 0, "show_full_form": 0, - "title": "Explore Stock Settings", + "title": "Review Stock Settings", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json new file mode 100644 index 00000000000..e684780751f --- /dev/null +++ b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json @@ -0,0 +1,24 @@ +{ + "action": "View Report", + "action_label": "Check Stock Projected Qty", + "creation": "2021-08-20 14:38:41.649103", + "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-08-20 14:38:41.649103", + "modified_by": "Administrator", + "name": "View Stock Projected Qty", + "owner": "Administrator", + "reference_report": "Stock Projected Qty", + "report_description": "You can set the filters to narrow the results, then click on Generate New Report to see the updated report.", + "report_reference_doctype": "Item", + "report_type": "Script Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Check Stock Projected Qty", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json new file mode 100644 index 00000000000..c46c4bdab86 --- /dev/null +++ b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2021-05-17 16:12:43.427579", + "description": "# View Warehouse\nIn ERPNext the term 'warehouse' can be thought of as a storage location.\n\nWarehouses are arranged in ERPNext in a tree like structure, where multiple sub-warehouses can be grouped under a single warehouse.\n\nIn this step we will view the [**Warehouse Tree**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse#21-tree-view) to view the [**Warehouses**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse) that are set by default.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 15:04:41.198413", + "modified_by": "Administrator", + "name": "View Warehouses", + "owner": "Administrator", + "path": "Tree/Warehouse", + "show_form_tour": 0, + "show_full_form": 0, + "title": "View Warehouses", + "validate_action": 1 +} \ No newline at end of file From 4c3034ad798b844c09109b954d0c474cd5fd342c Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 17:47:51 +0530 Subject: [PATCH 23/48] fix: remove VARCHARs from Sales Invoice (#27136) (#27139) Sales Invoice doctype is starting to hit row length limit as many integrations add custom fields on this doctype. This is just a small change to remove VARCHAR(140) fields and reduce row size wherever possible. (cherry picked from commit 8d116fb9ff45b193d4dad75db700439bc6fa5799) Co-authored-by: Ankush Menat --- .../doctype/sales_invoice/sales_invoice.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 01ae713cd36..99ecb8a4fea 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -247,7 +247,7 @@ "depends_on": "customer", "fetch_from": "customer.customer_name", "fieldname": "customer_name", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "in_global_search": 1, @@ -692,10 +692,11 @@ { "fieldname": "scan_barcode", "fieldtype": "Data", - "options": "Barcode", "hide_days": 1, "hide_seconds": 1, - "label": "Scan Barcode" + "label": "Scan Barcode", + "length": 1, + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -1059,6 +1060,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Apply Additional Discount On", + "length": 15, "options": "\nGrand Total\nNet Total", "print_hide": 1 }, @@ -1145,7 +1147,7 @@ { "description": "In Words will be visible once you save the Sales Invoice.", "fieldname": "base_in_words", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "label": "In Words (Company Currency)", @@ -1205,7 +1207,7 @@ }, { "fieldname": "in_words", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "label": "In Words", @@ -1558,6 +1560,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Print Language", + "length": 6, "print_hide": 1, "read_only": 1 }, @@ -1645,6 +1648,7 @@ "hide_seconds": 1, "in_standard_filter": 1, "label": "Status", + "length": 30, "no_copy": 1, "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1, @@ -1704,6 +1708,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Is Opening Entry", + "length": 4, "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", @@ -1715,6 +1720,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "C-Form Applicable", + "length": 4, "no_copy": 1, "options": "No\nYes", "print_hide": 1 @@ -2017,7 +2023,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-08-18 16:07:45.122570", + "modified": "2021-08-25 14:46:05.279588", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From c703ce1c618bb416d28b3ec315ed759a420db27f Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 25 Aug 2021 18:58:56 +0530 Subject: [PATCH 24/48] fix: get filters to work - reorder and rename columns - add work order filter --- .../process_loss_report.js | 7 ++++ .../process_loss_report.py | 40 +++++++++---------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/stock/report/process_loss_report/process_loss_report.js index 078b9e11ce9..b0c2b94a254 100644 --- a/erpnext/stock/report/process_loss_report/process_loss_report.js +++ b/erpnext/stock/report/process_loss_report/process_loss_report.js @@ -17,6 +17,13 @@ frappe.query_reports["Process Loss Report"] = { fieldname: "item", fieldtype: "Link", options: "Item", + mandatory: false, + }, + { + label: __("Work Order"), + fieldname: "work_order", + fieldtype: "Link", + options: "Work Order", mandatory: false, }, { diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/stock/report/process_loss_report/process_loss_report.py index be0f0151d40..7494328ab43 100644 --- a/erpnext/stock/report/process_loss_report/process_loss_report.py +++ b/erpnext/stock/report/process_loss_report/process_loss_report.py @@ -43,12 +43,6 @@ def get_columns() -> Columns: 'fieldtype': 'Data', 'width': '100' }, - { - 'label': 'Qty To Manufacture', - 'fieldname': 'qty', - 'fieldtype': 'Float', - 'width': '150' - }, { 'label': 'Manufactured Qty', 'fieldname': 'produced_qty', @@ -56,7 +50,7 @@ def get_columns() -> Columns: 'width': '150' }, { - 'label': 'Process Loss Qty', + 'label': 'Loss Qty', 'fieldname': 'process_loss_qty', 'fieldtype': 'Float', 'width': '150' @@ -68,23 +62,23 @@ def get_columns() -> Columns: 'width': '150' }, { - 'label': 'Total FG Value', + 'label': 'Loss Value', + 'fieldname': 'total_pl_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'FG Value', 'fieldname': 'total_fg_value', 'fieldtype': 'Float', 'width': '150' }, { - 'label': 'Total Raw Material Value', + 'label': 'Raw Material Value', 'fieldname': 'total_rm_value', 'fieldtype': 'Float', 'width': '150' - }, - { - 'label': 'Total Process Loss Value', - 'fieldname': 'total_pl_value', - 'fieldtype': 'Float', - 'width': '150' - }, + } ] def get_query_args(filters: Filters) -> QueryArgs: @@ -111,10 +105,11 @@ def run_query(query_args: QueryArgs) -> Data: AND wo.company = %(company)s AND se.docstatus = 1 AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s - %(item_filter)s + {item_filter} + {work_order_filter} GROUP BY se.work_order - """, query_args, as_dict=1) + """.format(**query_args), query_args, as_dict=1, debug=1) def update_data_with_total_pl_value(data: Data) -> None: for row in data: @@ -122,11 +117,16 @@ def update_data_with_total_pl_value(data: Data) -> None: row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg def get_filter_conditions(filters: Filters) -> QueryArgs: - filter_conditions = dict(item_filter="") + filter_conditions = dict(item_filter="", work_order_filter="") if "item" in filters: production_item = filters.get("item") filter_conditions.update( - {"item_filter": f"wo.production_item='{production_item}'"} + {"item_filter": f"AND wo.production_item='{production_item}'"} + ) + if "work_order" in filters: + work_order_name = filters.get("work_order") + filter_conditions.update( + {"work_order_filter": f"AND wo.name='{work_order_name}'"} ) return filter_conditions From f8ec0b6a86acbee91f26e5961291424350e78a83 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 20:05:54 +0530 Subject: [PATCH 25/48] feat: provision to create customer from opportunity (#27145) * feat: provision to create customer from opportunity (#27141) * feat: provision to create customer from opportunity * fead: linking of address and contact * revert: create_opportunity_address_contact * enabming print hide and no copy (cherry picked from commit 4d98be2126572a32d13ab76db5954d674d6845b2) # Conflicts: # erpnext/crm/doctype/opportunity/opportunity.js * Update opportunity.js * fix: conflicts Co-authored-by: Anupam Kumar Co-authored-by: Rucha Mahabal --- .../crm/doctype/opportunity/opportunity.js | 19 +++++++++++++++++-- .../crm/doctype/opportunity/opportunity.py | 18 ++++++++++++++++++ .../selling/doctype/customer/customer.json | 11 ++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 875d221efeb..5cc63d4511b 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -95,9 +95,17 @@ frappe.ui.form.on("Opportunity", { }, __('Create')); } - frm.add_custom_button(__('Quotation'), - cur_frm.cscript.create_quotation, __('Create')); + if (frm.doc.opportunity_from != "Customer") { + frm.add_custom_button(__('Customer'), + function() { + frm.trigger("make_customer") + }, __('Create')); + } + frm.add_custom_button(__('Quotation'), + function() { + frm.trigger("create_quotation") + }, __('Create')); } if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { @@ -194,6 +202,13 @@ erpnext.crm.Opportunity = frappe.ui.form.Controller.extend({ method: "erpnext.crm.doctype.opportunity.opportunity.make_quotation", frm: cur_frm }) + }, + + make_customer: function() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_customer", + frm: cur_frm + }) } }); diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 8ce482a3f9f..a74a94afd68 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -287,6 +287,24 @@ def make_request_for_quotation(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_customer(source_name, target_doc=None): + def set_missing_values(source, target): + if source.opportunity_from == "Lead": + target.lead_name = source.party_name + + doclist = get_mapped_doc("Opportunity", source_name, { + "Opportunity": { + "doctype": "Customer", + "field_map": { + "currency": "default_currency", + "customer_name": "customer_name" + } + } + }, target_doc, set_missing_values) + + return doclist + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): doclist = get_mapped_doc("Opportunity", source_name, { diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index cd94ee101af..0d839fc8228 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -20,6 +20,7 @@ "tax_withholding_category", "default_bank_account", "lead_name", + "opportunity_name", "image", "column_break0", "account_manager", @@ -493,6 +494,14 @@ "fieldtype": "Link", "label": "Tax Withholding Category", "options": "Tax Withholding Category" + }, + { + "fieldname": "opportunity_name", + "fieldtype": "Link", + "label": "From Opportunity", + "no_copy": 1, + "options": "Opportunity", + "print_hide": 1 } ], "icon": "fa fa-user", @@ -500,7 +509,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-28 12:54:57.258959", + "modified": "2021-08-25 18:56:09.929905", "modified_by": "Administrator", "module": "Selling", "name": "Customer", From 0fe6995816fc9b136d8c0fc17a118637af6ac50b Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 20:11:23 +0530 Subject: [PATCH 26/48] fix: sequence of sub-operations in job card (#27138) (#27147) (cherry picked from commit ad45ddcabe90c628344474ca5022fd67e27f43ad) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/job_card/job_card.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 66e2394b847..3efbe88adaf 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -26,17 +26,17 @@ class JobCard(Document): self.set_status() self.validate_operation_id() self.validate_sequence_id() - self.get_sub_operations() + self.set_sub_operations() self.update_sub_operation_status() - def get_sub_operations(self): + def set_sub_operations(self): if self.operation: self.sub_operations = [] - for row in frappe.get_all("Sub Operation", - filters = {"parent": self.operation}, fields=["operation", "idx"]): - row.status = "Pending" + for row in frappe.get_all('Sub Operation', + filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'): + row.status = 'Pending' row.sub_operation = row.operation - self.append("sub_operations", row) + self.append('sub_operations', row) def validate_time_logs(self): self.total_time_in_mins = 0.0 @@ -690,7 +690,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.set('time_logs', []) target.set('employee', []) target.set('items', []) - target.get_sub_operations() + target.set_sub_operations() target.get_required_items() target.validate_time_logs() From 7b9a23eb7aabc926fab79db17e3c9638aeb3972a Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 25 Aug 2021 21:15:44 +0530 Subject: [PATCH 27/48] feat: Increase number of supported currency exchanges (#26763) * fix: update test suite to accodomate new currency exchange function * feat: Increase number of supported currency exchanges * fix: don't make api call when testing * remove condition for test(being fixed in another pull request) --- .../test_currency_exchange.py | 23 +++++-------------- erpnext/setup/utils.py | 14 +++++------ 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index 1c928cd87d0..4ff2dd7e0e9 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -63,11 +63,11 @@ class TestCurrencyExchange(unittest.TestCase): exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) - # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io + # Exchange rate as on 15th Dec, 2015 self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling") self.assertFalse(exchange_rate == 60) - self.assertEqual(flt(exchange_rate, 3), 66.894) + self.assertEqual(flt(exchange_rate, 3), 66.999) def test_exchange_rate_strict(self): # strict currency settings @@ -77,28 +77,17 @@ class TestCurrencyExchange(unittest.TestCase): exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying") self.assertEqual(exchange_rate, 60.0) - # Will fetch from fixer.io self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 67.79) + self.assertEqual(flt(exchange_rate, 3), 67.235) exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) - # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io + # Exchange rate as on 15th Dec, 2015 self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 66.894) - - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10", "for_selling") - self.assertEqual(exchange_rate, 65.1) - - # NGN is not available on fixer.io so these should return 0 - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09", "for_selling") - self.assertEqual(exchange_rate, 0) - - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11", "for_selling") - self.assertEqual(exchange_rate, 0) + self.assertEqual(flt(exchange_rate, 3), 66.999) def test_exchange_rate_strict_switched(self): # Start with allow_stale is True @@ -111,4 +100,4 @@ class TestCurrencyExchange(unittest.TestCase): # Will fetch from fixer.io self.clear_cache() exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") - self.assertEqual(flt(exchange_rate, 3), 67.79) + self.assertEqual(flt(exchange_rate, 3), 67.235) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 13269a82823..27237bf2cbe 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -93,21 +93,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No try: cache = frappe.cache() - key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date,from_currency, to_currency) + key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) value = cache.get(key) if not value: import requests - api_url = "https://frankfurter.app/{0}".format(transaction_date) + api_url = "https://api.exchangerate.host/convert" response = requests.get(api_url, params={ - "base": from_currency, - "symbols": to_currency + "date": transaction_date, + "from": from_currency, + "to": to_currency }) # expire in 6 hours response.raise_for_status() - value = response.json()["rates"][to_currency] - - cache.set_value(key, value, expires_in_sec=6 * 60 * 60) + value = response.json()["result"] + cache.setex(name=key, time=21600, value=flt(value)) return flt(value) except: frappe.log_error(title="Get Exchange Rate") From 4837d8872e5454a911e069c3670b0c5f5734f49a Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 22:25:29 +0530 Subject: [PATCH 28/48] fix: unable to create manual / auto asset depreciation entry when cost_center is mandatory (#26912) (#27149) Summary : unable to create manual / auto asset depreciation entry when cost_center is mandatory Reason: Though we are calculating value for depreciation_cost_center, it is not passed in credit_entry(it is passed in debit_entry) and this prevents creation of manual / auto asset depreciation entry when cost_center is mandatory Solution : pass already calculated depreciation_cost_center value in credit_entry (in line with, already done as in debit_entry) (cherry picked from commit b99c0119475ed82895f479f89685977204fcdbba) Co-authored-by: Ashish Shah --- erpnext/assets/doctype/asset/depreciation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 8f0afb42b2c..251fe3fa493 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -59,7 +59,7 @@ def make_depreciation_entry(asset_name, date=None): "credit_in_account_currency": d.depreciation_amount, "reference_type": "Asset", "reference_name": asset.name, - "cost_center": "" + "cost_center": depreciation_cost_center } debit_entry = { From 7f27586cbe6f3112183228c53b4f46c39299850e Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 22:26:09 +0530 Subject: [PATCH 29/48] fix(healthcare): Removed ignore user permissions flag in appointment (#27146) * fix(healthcare): Removed ignore user permissions flag in appointment (#27129) (cherry picked from commit 81b28b899843dcbf66d5c2cc241429e363ab04ac) # Conflicts: # erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py * chore: Fix merge conflicts Co-authored-by: Chillar Anand --- .../healthcare_practitioner.json | 5 ++- .../patient_appointment.json | 1 - .../test_patient_appointment.py | 37 ++++++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json index 8162f03f6dc..cb455eb5014 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json @@ -282,7 +282,7 @@ ], "image_field": "image", "links": [], - "modified": "2021-01-22 10:14:43.187675", + "modified": "2021-08-24 10:42:08.513054", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Practitioner", @@ -295,6 +295,7 @@ "read": 1, "report": 1, "role": "Laboratory User", + "select": 1, "share": 1, "write": 1 }, @@ -307,6 +308,7 @@ "read": 1, "report": 1, "role": "Physician", + "select": 1, "share": 1, "write": 1 }, @@ -319,6 +321,7 @@ "read": 1, "report": 1, "role": "Nursing User", + "select": 1, "share": 1, "write": 1 } diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 0267e65a212..a6929c28511 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -142,7 +142,6 @@ { "fieldname": "practitioner", "fieldtype": "Link", - "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Healthcare Practitioner", "options": "Healthcare Practitioner", diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index c0fe5f08854..062a32a92e6 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -251,6 +251,27 @@ class TestPatientAppointment(unittest.TestCase): appointment = create_appointment(patient, practitioner, nowdate(), service_unit=overlap_service_unit, save=0) self.assertRaises(MaximumCapacityError, appointment.save) + def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): + patient, practitioner = create_healthcare_docs() + create_appointment(patient, practitioner, nowdate()) + + patient, new_practitioner = create_healthcare_docs(id=2) + create_appointment(patient, new_practitioner, nowdate()) + + roles = [{"doctype": "Has Role", "role": "Physician"}] + user = create_user(roles=roles) + new_practitioner = frappe.get_doc('Healthcare Practitioner', new_practitioner) + new_practitioner.user_id = user.email + new_practitioner.save() + + frappe.set_user(user.name) + appointments = frappe.get_list('Patient Appointment') + assert len(appointments) == 1 + + frappe.set_user("Administrator") + appointments = frappe.get_list('Patient Appointment') + assert len(appointments) == 2 + def create_healthcare_docs(id=0): patient = create_patient(id) @@ -298,7 +319,6 @@ def create_practitioner(id=0, medical_department=None): return practitioner.name - def create_encounter(appointment): if appointment: encounter = frappe.new_doc('Patient Encounter') @@ -313,7 +333,6 @@ def create_encounter(appointment): return encounter - def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, appointment_type=None, save=1, department=None): item = create_healthcare_service_items() @@ -423,3 +442,17 @@ def create_service_unit(id=0, service_unit_type=None, service_unit_capacity=0): service_unit.save(ignore_permissions=True) return service_unit.name + +def create_user(email=None, roles=None): + if not email: + email = '{}@frappe.com'.format(frappe.utils.random_string(10)) + user = frappe.db.exists('User', email) + if not user: + user = frappe.get_doc({ + "doctype": "User", + "email": email, + "first_name": "test_user", + "password": "password", + "roles": roles, + }).insert() + return user From 6609321399da1fc6e6a9357b1470ffca21e0b7fa Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 26 Aug 2021 11:02:01 +0530 Subject: [PATCH 30/48] fix: removing toggle_display for address and contact HTML (#27152) (#27155) (cherry picked from commit c8f22e5524980996eec998566d2cea19be1f2b93) Co-authored-by: Anupam Kumar --- erpnext/selling/doctype/customer/customer.js | 1 - erpnext/selling/doctype/customer/customer.json | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index fb027be11c7..8416901b7cb 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -111,7 +111,6 @@ frappe.ui.form.on("Customer", { } frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Customer'} - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); if(!frm.doc.__islocal) { frappe.contacts.render_address_and_contact(frm); diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 0d839fc8228..2acc64cb433 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -268,6 +268,7 @@ "options": "fa fa-map-marker" }, { + "depends_on": "eval: !doc.__islocal", "fieldname": "address_html", "fieldtype": "HTML", "label": "Address HTML", @@ -284,6 +285,7 @@ "width": "50%" }, { + "depends_on": "eval: !doc.__islocal", "fieldname": "contact_html", "fieldtype": "HTML", "label": "Contact HTML", From 16fbee30a6cae38e1e3264272441af226b49f377 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 26 Aug 2021 12:23:52 +0530 Subject: [PATCH 31/48] fix: Shopping cart Exchange rate validation (#27050) * fix: Shopping cart Exchange rate validation - Use `get_exchange_rate` to check for price list exchange rate in cart settings - Move cart exchange rate validation for Price List from hooks to doc event - Call cart exchange rate validation on PL update only if PL is in cart and currency is changed * chore: Comment out obsolete test - Modifying this test means considering extreme edge cases, which seems pointless now * fix: Remove snippet that got in due to cherry-pick from `develop` - This snippet is not present in v13-hotfix. Via https://github.com/frappe/erpnext/pull/26520 Co-authored-by: Nabin Hait --- erpnext/hooks.py | 2 +- .../shopping_cart_settings.py | 61 ++++++++----------- .../test_shopping_cart_settings.py | 26 +++++--- .../stock/doctype/price_list/price_list.py | 14 +++++ 4 files changed, 57 insertions(+), 46 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 748bd088077..aede8ff2f46 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -243,7 +243,7 @@ doc_events = { "on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions", "erpnext.portal.utils.set_default_role"] }, - ("Sales Taxes and Charges Template", 'Price List'): { + "Sales Taxes and Charges Template": { "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" }, "Website Settings": { diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py index 2a497225fbc..efed1968a14 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint -from frappe.utils import comma_and +from frappe.utils import flt from frappe.model.document import Document from frappe.utils import get_datetime, get_datetime_str, now_datetime @@ -18,46 +18,35 @@ class ShoppingCartSettings(Document): def validate(self): if self.enabled: - self.validate_exchange_rates_exist() + self.validate_price_list_exchange_rate() + + def validate_price_list_exchange_rate(self): + "Check if exchange rate exists for Price List currency (to Company's currency)." + from erpnext.setup.utils import get_exchange_rate + + if not self.enabled or not self.company or not self.price_list: + return # this function is also called from hooks, check values again + + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency") - def validate_exchange_rates_exist(self): - """check if exchange rates exist for all Price List currencies (to company's currency)""" - company_currency = frappe.get_cached_value('Company', self.company, "default_currency") if not company_currency: - msgprint(_("Please specify currency in Company") + ": " + self.company, - raise_exception=ShoppingCartSetupError) + msg = f"Please specify currency in Company {self.company}" + frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - price_list_currency_map = frappe.db.get_values("Price List", - [self.price_list], "currency") + if not price_list_currency: + msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}" + frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - price_list_currency_map = dict(price_list_currency_map) + if price_list_currency != company_currency: + from_currency, to_currency = price_list_currency, company_currency - # check if all price lists have a currency - for price_list, currency in price_list_currency_map.items(): - if not currency: - frappe.throw(_("Currency is required for Price List {0}").format(price_list)) + # Get exchange rate checks Currency Exchange Records too + exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling") - expected_to_exist = [currency + "-" + company_currency - for currency in price_list_currency_map.values() - if currency != company_currency] - - # manqala 20/09/2016: set up selection parameters for query from tabCurrency Exchange - from_currency = [currency for currency in price_list_currency_map.values() if currency != company_currency] - to_currency = company_currency - # manqala end - - if expected_to_exist: - # manqala 20/09/2016: modify query so that it uses date in the selection from Currency Exchange. - # exchange rates defined with date less than the date on which this document is being saved will be selected - exists = frappe.db.sql_list("""select CONCAT(from_currency,'-',to_currency) from `tabCurrency Exchange` - where from_currency in (%s) and to_currency = "%s" and date <= curdate()""" % (", ".join(["%s"]*len(from_currency)), to_currency), tuple(from_currency)) - # manqala end - - missing = list(set(expected_to_exist).difference(exists)) - - if missing: - msgprint(_("Missing Currency Exchange Rates for {0}").format(comma_and(missing)), - raise_exception=ShoppingCartSetupError) + if not flt(exchange_rate): + msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}" + frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError) def validate_tax_rule(self): if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"): @@ -71,7 +60,7 @@ class ShoppingCartSettings(Document): def get_shipping_rules(self, shipping_territory): return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule") -def validate_cart_settings(doc, method): +def validate_cart_settings(doc=None, method=None): frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate") def get_shopping_cart_settings(): diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py index 008751e2088..9965e1af672 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py @@ -16,17 +16,25 @@ class TestShoppingCartSettings(unittest.TestCase): return frappe.get_doc({"doctype": "Shopping Cart Settings", "company": "_Test Company"}) - def test_exchange_rate_exists(self): - frappe.db.sql("""delete from `tabCurrency Exchange`""") + # NOTE: Exchangrate API has all enabled currencies that ERPNext supports. + # We aren't checking just currency exchange record anymore + # while validating price list currency exchange rate to that of company. + # The API is being used to fetch the rate which again almost always + # gives back a valid value (for valid currencies). + # This makes the test obsolete. + # Commenting because im not sure if there's a better test we can write - cart_settings = self.get_cart_settings() - cart_settings.price_list = "_Test Price List Rest of the World" - self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist) + # def test_exchange_rate_exists(self): + # frappe.db.sql("""delete from `tabCurrency Exchange`""") - from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ - currency_exchange_records - frappe.get_doc(currency_exchange_records[0]).insert() - cart_settings.validate_exchange_rates_exist() + # cart_settings = self.get_cart_settings() + # cart_settings.price_list = "_Test Price List Rest of the World" + # self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate) + + # from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ + # currency_exchange_records + # frappe.get_doc(currency_exchange_records[0]).insert() + # cart_settings.validate_price_list_exchange_rate() def test_tax_rule_validation(self): frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 10abde17eb2..002d3d898eb 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -13,6 +13,9 @@ class PriceList(Document): if not cint(self.buying) and not cint(self.selling): throw(_("Price List must be applicable for Buying or Selling")) + if not self.is_new(): + self.check_impact_on_shopping_cart() + def on_update(self): self.set_default_if_missing() self.update_item_price() @@ -32,6 +35,17 @@ class PriceList(Document): buying=%s, selling=%s, modified=NOW() where price_list=%s""", (self.currency, cint(self.buying), cint(self.selling), self.name)) + def check_impact_on_shopping_cart(self): + "Check if Price List currency change impacts Shopping Cart." + from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import validate_cart_settings + + doc_before_save = self.get_doc_before_save() + currency_changed = self.currency != doc_before_save.currency + affects_cart = self.name == frappe.get_cached_value("Shopping Cart Settings", None, "price_list") + + if currency_changed and affects_cart: + validate_cart_settings() + def on_trash(self): self.delete_price_list_details_key() From 5a066182809be3b8715dee35cd6aa15a89ac819b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:55:24 +0530 Subject: [PATCH 32/48] feat: initialize party link for customer & suppliers --- .../accounts/doctype/party_link/__init__.py | 0 .../accounts/doctype/party_link/party_link.js | 41 ++++++++++ .../doctype/party_link/party_link.json | 78 +++++++++++++++++++ .../accounts/doctype/party_link/party_link.py | 12 +++ .../doctype/party_link/test_party_link.py | 8 ++ 5 files changed, 139 insertions(+) create mode 100644 erpnext/accounts/doctype/party_link/__init__.py create mode 100644 erpnext/accounts/doctype/party_link/party_link.js create mode 100644 erpnext/accounts/doctype/party_link/party_link.json create mode 100644 erpnext/accounts/doctype/party_link/party_link.py create mode 100644 erpnext/accounts/doctype/party_link/test_party_link.py diff --git a/erpnext/accounts/doctype/party_link/__init__.py b/erpnext/accounts/doctype/party_link/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js new file mode 100644 index 00000000000..966a5f5d30b --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.js @@ -0,0 +1,41 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Party Link', { + refresh: function(frm) { + frm.set_query('party_type', 'links', () => { + return { + filters: { + name: ['in', party_types] + } + }; + }); + + frm.set_query('primary_role', () => { + return { + filters: { + name: ['in', ['Customer', 'Supplier']] + } + }; + }); + + frm.set_query('secondary_role', () => { + let party_types = Object.keys(frappe.boot.party_account_types) + .filter(p => p != frm.doc.primary_role); + return { + filters: { + name: ['in', party_types] + } + }; + }); + }, + + primary_role(frm) { + frm.set_value('primary_party', ''); + frm.set_value('secondary_role', ''); + }, + + secondary_role(frm) { + frm.set_value('secondary_party', ''); + } +}); diff --git a/erpnext/accounts/doctype/party_link/party_link.json b/erpnext/accounts/doctype/party_link/party_link.json new file mode 100644 index 00000000000..2053dc0f00a --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.json @@ -0,0 +1,78 @@ +{ + "actions": [], + "autoname": "ACC-PT-LNK-.###.", + "creation": "2021-08-18 21:06:53.027695", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "primary_role", + "secondary_role", + "column_break_2", + "primary_party", + "secondary_party" + ], + "fields": [ + { + "fieldname": "primary_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Primary Role", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "depends_on": "primary_role", + "fieldname": "secondary_role", + "fieldtype": "Link", + "label": "Secondary Role", + "mandatory_depends_on": "primary_role", + "options": "DocType" + }, + { + "depends_on": "primary_role", + "fieldname": "primary_party", + "fieldtype": "Dynamic Link", + "label": "Primary Party", + "mandatory_depends_on": "primary_role", + "options": "primary_role" + }, + { + "depends_on": "secondary_role", + "fieldname": "secondary_party", + "fieldtype": "Dynamic Link", + "label": "Secondary Party", + "mandatory_depends_on": "secondary_role", + "options": "secondary_role" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-19 17:53:43.456752", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Party Link", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "primary_party", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py new file mode 100644 index 00000000000..80f86e75a02 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -0,0 +1,12 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +class PartyLink(Document): + def validate(self): + if self.primary_role not in ['Customer', 'Supplier']: + frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), + title=_("Invalid Primary Role")) diff --git a/erpnext/accounts/doctype/party_link/test_party_link.py b/erpnext/accounts/doctype/party_link/test_party_link.py new file mode 100644 index 00000000000..a3ea3959ba4 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/test_party_link.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestPartyLink(unittest.TestCase): + pass From fb94726d265f51d1822d4efd14e94c5bd123fffd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:56:04 +0530 Subject: [PATCH 33/48] feat: toggle to enable common party accounting --- .../doctype/accounts_settings/accounts_settings.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 49a2afee85f..935e29a9d33 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -18,6 +18,7 @@ "delete_linked_ledger_entries", "book_asset_depreciation_entry_automatically", "unlink_advance_payment_on_cancelation_of_order", + "enable_common_party_accounting", "post_change_gl_entries", "enable_discount_accounting", "tax_settings_section", @@ -269,6 +270,12 @@ "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting" + }, + { + "default": "0", + "fieldname": "enable_common_party_accounting", + "fieldtype": "Check", + "label": "Enable Common Party Accounting" } ], "icon": "icon-cog", @@ -276,7 +283,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-07-12 18:54:29.084958", + "modified": "2021-08-19 11:17:38.788054", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 2cb8cc525ce05954a48a41f1ca40f08889a54cdd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:57:30 +0530 Subject: [PATCH 34/48] feat: auto create advance entry on invoice submission --- .../purchase_invoice/purchase_invoice.py | 2 + .../doctype/sales_invoice/sales_invoice.py | 2 + erpnext/controllers/accounts_controller.py | 62 ++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5094b1752b4..fdd8765b411 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -413,6 +413,8 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.process_common_party_accounting() + def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6e643db8d72..9e295d5ae54 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -253,6 +253,8 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_submit") + self.process_common_party_accounting() + def validate_pos_return(self): if self.is_pos and self.is_return: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b17d1868d99..7be3981dd92 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -14,7 +14,7 @@ from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_a from erpnext.utilities.transaction_base import TransactionBase from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.sales_and_purchase_return import validate_return -from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled +from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_account from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, apply_pricing_rule_for_free_items, get_applied_pricing_rules) from erpnext.exceptions import InvalidCurrency @@ -1368,6 +1368,66 @@ class AccountsController(TransactionBase): return False + def process_common_party_accounting(self): + is_invoice = self.doctype in ['Sales Invoice', 'Purchase Invoice'] + if not is_invoice: + return + + if frappe.db.get_single_value('Accounts Settings', 'enable_common_party_accounting'): + party_link = self.get_common_party_link() + if party_link and self.outstanding_amount: + self.create_advance_and_reconcile(party_link) + + def get_common_party_link(self): + party_type, party = self.get_party() + party_link = frappe.db.exists('Party Link', {'secondary_role': party_type, 'secondary_party': party}) + if party_link: + return frappe.db.get_value('Party Link', party_link, ['primary_role', 'primary_party'], as_dict=True) + + def create_advance_and_reconcile(self, party_link): + secondary_party_type, secondary_party = self.get_party() + primary_party_type, primary_party = party_link.primary_role, party_link.primary_party + + primary_account = get_party_account(primary_party_type, primary_party, self.company) + secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) + + jv = frappe.new_doc('Journal Entry') + jv.voucher_type = 'Journal Entry' + jv.naming_series = 'ACC-JV-.YYYY.-' + jv.posting_date = self.posting_date + jv.company = self.company + jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name) + + reconcilation_entry = frappe._dict() + advance_entry = frappe._dict() + cost_center = erpnext.get_default_cost_center(self.company) + + reconcilation_entry.account = secondary_account + reconcilation_entry.party_type = secondary_party_type + reconcilation_entry.party = secondary_party + reconcilation_entry.reference_type = self.doctype + reconcilation_entry.reference_name = self.name + reconcilation_entry.cost_center = cost_center + + advance_entry.account = primary_account + advance_entry.party_type = primary_party_type + advance_entry.party = primary_party + advance_entry.cost_center = cost_center + advance_entry.is_advance = 'Yes' + + if self.doctype == 'Sales Invoice': + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + advance_entry.debit_in_account_currency = self.outstanding_amount + else: + advance_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + + jv.append('accounts', reconcilation_entry) + jv.append('accounts', advance_entry) + + jv.save() + jv.submit() + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) From da54f0a58302f11d524bb4b3928f5c835128df2b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 17:57:54 +0530 Subject: [PATCH 35/48] test: creation of advance entry on invoice submission --- .../sales_invoice/test_sales_invoice.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5bbde09c1f7..ef6a6d72abb 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2071,6 +2071,50 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) enable_discount_accounting(enable=0) + def test_sales_invoice_against_supplier(self): + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import make_customer + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # create a customer + customer = make_customer(customer="_Test Common Supplier") + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Supplier").name + + # create a party link between customer & supplier + # set primary role as supplier + party_link = frappe.new_doc("Party Link") + party_link.primary_role = "Supplier" + party_link.primary_party = supplier + party_link.secondary_role = "Customer" + party_link.secondary_party = customer + party_link.save() + + # enable common party accounting + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) + + # create a sales invoice + si = create_sales_invoice(customer=customer) + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, 'Paid') + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all('Journal Entry Account', { + 'account': si.debit_to, + 'party_type': 'Customer', + 'party': si.customer, + 'reference_type': si.doctype, + 'reference_name': si.name + }, pluck='credit_in_account_currency') + + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + party_link.delete() + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' From a87139e16a08f765fba9859d06ed91b1a995c276 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 19 Aug 2021 18:10:02 +0530 Subject: [PATCH 36/48] fix: remove unwanted filter query --- erpnext/accounts/doctype/party_link/party_link.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js index 966a5f5d30b..6da9291d64d 100644 --- a/erpnext/accounts/doctype/party_link/party_link.js +++ b/erpnext/accounts/doctype/party_link/party_link.js @@ -3,14 +3,6 @@ frappe.ui.form.on('Party Link', { refresh: function(frm) { - frm.set_query('party_type', 'links', () => { - return { - filters: { - name: ['in', party_types] - } - }; - }); - frm.set_query('primary_role', () => { return { filters: { From f5afd51fa4c5d985fb70b83662e454673c328b33 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:08:28 +0530 Subject: [PATCH 37/48] feat: validate multiple links --- erpnext/accounts/doctype/party_link/party_link.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py index 80f86e75a02..7d58506ce74 100644 --- a/erpnext/accounts/doctype/party_link/party_link.py +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -10,3 +10,17 @@ class PartyLink(Document): if self.primary_role not in ['Customer', 'Supplier']: frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), title=_("Invalid Primary Role")) + + existing_party_link = frappe.get_all('Party Link', { + 'primary_party': self.secondary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.secondary_role, self.secondary_party, existing_party_link[0])) + + existing_party_link = frappe.get_all('Party Link', { + 'secondary_party': self.primary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.primary_role, self.primary_party, existing_party_link[0])) From e0649c132ec30523989124967311650d8462811d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:10:19 +0530 Subject: [PATCH 38/48] fix: party link permissions --- .../doctype/party_link/party_link.json | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/party_link/party_link.json b/erpnext/accounts/doctype/party_link/party_link.json index 2053dc0f00a..a1bb15f0d6b 100644 --- a/erpnext/accounts/doctype/party_link/party_link.json +++ b/erpnext/accounts/doctype/party_link/party_link.json @@ -52,7 +52,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-08-19 17:53:43.456752", + "modified": "2021-08-25 20:08:56.761150", "modified_by": "Administrator", "module": "Accounts", "name": "Party Link", @@ -69,6 +69,30 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 } ], "sort_field": "modified", From fb6af0481ce9bdf419174cda60aa0ccaecc154c9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:15:23 +0530 Subject: [PATCH 39/48] perf: reduce number of queries to get party link --- erpnext/controllers/accounts_controller.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7be3981dd92..f8acbec7b49 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1380,9 +1380,12 @@ class AccountsController(TransactionBase): def get_common_party_link(self): party_type, party = self.get_party() - party_link = frappe.db.exists('Party Link', {'secondary_role': party_type, 'secondary_party': party}) - if party_link: - return frappe.db.get_value('Party Link', party_link, ['primary_role', 'primary_party'], as_dict=True) + return frappe.db.get_value( + doctype='Party Link', + filters={'secondary_role': party_type, 'secondary_party': party}, + fieldname=['primary_role', 'primary_party'], + as_dict=True + ) def create_advance_and_reconcile(self, party_link): secondary_party_type, secondary_party = self.get_party() From 970e5af05187a6ca7e1039ba184170b75e5cf977 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 20:17:04 +0530 Subject: [PATCH 40/48] fix: cost center & naming series --- erpnext/controllers/accounts_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f8acbec7b49..e02e7351520 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1396,26 +1396,24 @@ class AccountsController(TransactionBase): jv = frappe.new_doc('Journal Entry') jv.voucher_type = 'Journal Entry' - jv.naming_series = 'ACC-JV-.YYYY.-' jv.posting_date = self.posting_date jv.company = self.company jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name) reconcilation_entry = frappe._dict() advance_entry = frappe._dict() - cost_center = erpnext.get_default_cost_center(self.company) reconcilation_entry.account = secondary_account reconcilation_entry.party_type = secondary_party_type reconcilation_entry.party = secondary_party reconcilation_entry.reference_type = self.doctype reconcilation_entry.reference_name = self.name - reconcilation_entry.cost_center = cost_center + reconcilation_entry.cost_center = self.cost_center advance_entry.account = primary_account advance_entry.party_type = primary_party_type advance_entry.party = primary_party - advance_entry.cost_center = cost_center + advance_entry.cost_center = self.cost_center advance_entry.is_advance = 'Yes' if self.doctype == 'Sales Invoice': From abde7fc085ca8fb3b9cb6744b6804eb9e2695af6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Aug 2021 21:35:32 +0530 Subject: [PATCH 41/48] fix: cost center in test_sales_invoice_against_supplier --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ef6a6d72abb..d008b2ae24f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2093,7 +2093,7 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) # create a sales invoice - si = create_sales_invoice(customer=customer) + si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC") # check outstanding of sales invoice si.reload() From 6636d857f7fbd4b7d3b3633d0f1feadac48a9e97 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 26 Aug 2021 13:12:51 +0530 Subject: [PATCH 42/48] fix: Don't create inward SLE against SI unless is internal customer enabled (#27086) * fix: Dont create inward SLE against SI unless is internal customer enabled - Check if is internal customer enabled apart from target warehouse - Test to check if inward SLE is made if target warehouse is accidentally set but customer is not internal * test: Use internal customer for delivery of bundle items to target warehouse - created `create_internal_customer` util - reused it in delivery note and sales invoice tests - use internal customer for target warehouse test in delivery note (cherry picked from commit f4dc9ee2aa57d82a0be747a89e1ca573940da959) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 142 +++++++++++++++--- erpnext/controllers/selling_controller.py | 2 +- .../selling/doctype/customer/test_customer.py | 25 +++ .../delivery_note/test_delivery_note.py | 25 ++- 4 files changed, 171 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5bbde09c1f7..bb98cf33203 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -148,7 +148,7 @@ class TestSalesInvoice(unittest.TestCase): si1 = create_sales_invoice(rate=1000) si2 = create_sales_invoice(rate=300) si3 = create_sales_invoice(qty=-1, rate=300, is_return=1) - + pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC") pe.append('references', { @@ -1783,23 +1783,12 @@ class TestSalesInvoice(unittest.TestCase): acc_settings.save() def test_inter_company_transaction(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer - if not frappe.db.exists("Customer", "_Test Internal Customer"): - customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Internal Customer", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": "_Test Company 1" - }) - - customer.append("companies", { - "company": "Wind Power LLC" - }) - - customer.insert() + create_internal_customer( + customer_name="_Test Internal Customer", + represents_company="_Test Company 1" + ) if not frappe.db.exists("Supplier", "_Test Internal Supplier"): supplier = frappe.get_doc({ @@ -1842,6 +1831,125 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") +<<<<<<< HEAD +======= + def test_inter_company_transaction_without_default_warehouse(self): + "Check mapping (expense account) of inter company SI to PI in absence of default warehouse." + # setup + old_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) + + old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled('_Test Company 1') + frappe.local.enable_perpetual_inventory['_Test Company 1'] = 1 + + frappe.db.set_value("Company", '_Test Company 1', "stock_received_but_not_billed", "Stock Received But Not Billed - _TC1") + frappe.db.set_value("Company", '_Test Company 1', "expenses_included_in_valuation", "Expenses Included In Valuation - _TC1") + + + if not frappe.db.exists("Customer", "_Test Internal Customer"): + customer = frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": "_Test Internal Customer", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": "_Test Company 1" + }) + + customer.append("companies", { + "company": "Wind Power LLC" + }) + + customer.insert() + + if not frappe.db.exists("Supplier", "_Test Internal Supplier"): + supplier = frappe.get_doc({ + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Internal Supplier", + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": "Wind Power LLC" + }) + + supplier.append("companies", { + "company": "_Test Company 1" + }) + + supplier.insert() + + # begin test + si = create_sales_invoice( + company = "Wind Power LLC", + customer = "_Test Internal Customer", + debit_to = "Debtors - WP", + warehouse = "Stores - WP", + income_account = "Sales - WP", + expense_account = "Cost of Goods Sold - WP", + cost_center = "Main - WP", + currency = "USD", + update_stock = 1, + do_not_save = 1 + ) + si.selling_price_list = "_Test Price List Rest of the World" + si.submit() + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + + # in absence of warehouse Stock Received But Not Billed is set as expense account while mapping + # mapping is not obstructed + self.assertIsNone(target_doc.items[0].warehouse) + self.assertEqual(target_doc.items[0].expense_account, "Stock Received But Not Billed - _TC1") + + target_doc.items[0].update({"cost_center": "Main - _TC1"}) + + # missing warehouse is validated on save, after mapping + self.assertRaises(WarehouseMissingError, target_doc.save) + + target_doc.items[0].update({"warehouse": "Stores - _TC1"}) + target_doc.save() + + # after warehouse is set, linked account or default inventory account is set + self.assertEqual(target_doc.items[0].expense_account, 'Stock In Hand - _TC1') + + # tear down + frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock) + + def test_sle_if_target_warehouse_exists_accidentally(self): + """ + Check if inward entry exists if Target Warehouse accidentally exists + but Customer is not an internal customer. + """ + se = make_stock_entry( + item_code="138-CMS Shoe", + target="Finished Goods - _TC", + company = "_Test Company", + qty=1, + basic_rate=500 + ) + + si = frappe.copy_doc(test_records[0]) + si.update_stock = 1 + si.set_warehouse = "Finished Goods - _TC" + si.set_target_warehouse = "Stores - _TC" + si.get("items")[0].warehouse = "Finished Goods - _TC" + si.get("items")[0].target_warehouse = "Stores - _TC" + si.insert() + si.submit() + + sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name}, + fields=["name", "actual_qty"]) + + # check if only one SLE for outward entry is created + self.assertEqual(len(sles), 1) + self.assertEqual(sles[0].actual_qty, -1) + + # tear down + si.cancel() + se.cancel() + +>>>>>>> f4dc9ee2aa (fix: Don't create inward SLE against SI unless is internal customer enabled (#27086)) def test_internal_transfer_gl_entry(self): ## Create internal transfer account account = create_account(account_name="Unrealized Profit", diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index fc2cc97e0a5..61bbee00471 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -422,7 +422,7 @@ class SellingController(StockController): or (cint(self.is_return) and self.docstatus==2)): sl_entries.append(self.get_sle_for_source_warehouse(d)) - if d.target_warehouse: + if d.target_warehouse and self.get("is_internal_customer"): sl_entries.append(self.get_sle_for_target_warehouse(d)) if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index b1a5b52f963..908ba270b04 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -352,3 +352,28 @@ def set_credit_limit(customer, company, credit_limit): 'credit_limit': credit_limit }) customer.credit_limits[-1].db_insert() + +def create_internal_customer(**args): + args = frappe._dict(args) + + customer_name = args.get("customer_name") or "_Test Internal Customer" + + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_group": args.customer_group or "_Test Customer Group", + "customer_name": customer_name, + "customer_type": args.customer_type or "Individual", + "territory": args.territory or "_Test Territory", + "is_internal_customer": 1, + "represents_company": args.represents_company or "_Test Company with perpetual inventory" + }) + + customer.append("companies", { + "company": args.allowed_company or "Wind Power LLC" + }) + customer.insert() + + return customer + else: + return frappe.get_cached_doc("Customer", customer_name) \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 91e7c006eef..dffc73020d6 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -430,12 +430,18 @@ class TestDeliveryNote(unittest.TestCase): }) def test_delivery_of_bundled_items_to_target_warehouse(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + customer = create_internal_customer( + customer_name="_Test Internal Customer 2", + allowed_company="_Test Company with perpetual inventory" + ) set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item Home Desktop 100", "FIFO") - target_warehouse=get_warehouse(company=company, abbr="TCP1", + target_warehouse = get_warehouse(company=company, abbr="TCP1", warehouse_name="_Test Customer Warehouse").name for warehouse in ("Stores - TCP1", target_warehouse): @@ -444,10 +450,16 @@ class TestDeliveryNote(unittest.TestCase): create_stock_reconciliation(item_code="_Test Item Home Desktop 100", company = company, expense_account = "Stock Adjustment - TCP1", warehouse=warehouse, qty=500, rate=100) - dn = create_delivery_note(item_code="_Test Product Bundle Item", - company='_Test Company with perpetual inventory', cost_center = 'Main - TCP1', - expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True, qty=5, rate=500, - warehouse="Stores - TCP1", target_warehouse=target_warehouse) + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + company="_Test Company with perpetual inventory", + customer=customer.name, + cost_center = 'Main - TCP1', + expense_account = "Cost of Goods Sold - TCP1", + do_not_submit=True, + qty=5, rate=500, + warehouse="Stores - TCP1", + target_warehouse=target_warehouse) dn.submit() @@ -487,6 +499,9 @@ class TestDeliveryNote(unittest.TestCase): for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) + # tear down + frappe.db.rollback() + def test_closed_delivery_note(self): from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status From f7f573b11bd902b9bcf3f13e456fb7ef91a4f7e7 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 26 Aug 2021 13:15:57 +0530 Subject: [PATCH 43/48] fix: prevent over riding scrap table values, name kwargs, set currency --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- erpnext/manufacturing/doctype/bom/test_bom.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b9647e9c613..70237f9147f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -232,7 +232,7 @@ class BOM(WebsiteGenerator): } ret = self.get_bom_material_detail(args) for key, value in ret.items(): - if not item.get(key): + if item.get(key) is None: item.set(key, value) @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 8b9820c7bb5..6a81ac33679 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -232,30 +232,30 @@ class TestBOM(unittest.TestCase): 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, 0.25, 0, 1 + 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, 2, 0 + fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 ) # PL Item qty can't be >= FG Item qty self.assertRaises(frappe.ValidationError, bom_doc.submit) bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, 1, 100 + 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, 0.25, 0 + fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0 ) # 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, 0.25, 0, is_process_loss=0 + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0 ) # FG Items in Scrap/Loss Table should have Is Process Loss set self.assertRaises(frappe.ValidationError, bom_doc.submit) @@ -330,6 +330,7 @@ def create_nested_bom(tree, prefix="_Test bom "): bom = frappe.get_doc(doctype="BOM", item=bom_item_code) for child_item in child_items.keys(): bom.append("items", {"item_code": prefix + child_item}) + bom.currency = "INR" bom.insert() bom.submit() @@ -373,6 +374,7 @@ def create_bom_with_process_loss_item( "rate": scrap_rate, "is_process_loss": is_process_loss }) + bom_doc.currency = "INR" return bom_doc def create_process_loss_bom_items(): From ed617d0939cb4bddfb724afcb7dff903fb026733 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 26 Aug 2021 13:27:39 +0530 Subject: [PATCH 44/48] fix(regional): minor fixes and test for South Africa VAT report (#26933) (#27162) --- .../vat_audit_report/test_vat_audit_report.py | 193 ++++++++++++++++++ .../vat_audit_report/vat_audit_report.py | 11 +- 2 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 erpnext/regional/report/vat_audit_report/test_vat_audit_report.py diff --git a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py new file mode 100644 index 00000000000..dea17a66fda --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py @@ -0,0 +1,193 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from unittest import TestCase +from frappe.utils import today + +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + +from erpnext.regional.report.vat_audit_report.vat_audit_report import execute + +class TestVATAuditReport(TestCase): + def setUp(self): + frappe.set_user("Administrator") + make_company("_Test Company SA VAT", "_TCSV") + + create_account(account_name="VAT - 0%", account_type="Tax", + parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + create_account(account_name="VAT - 15%", account_type="Tax", + parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + set_sa_vat_accounts() + + make_item("_Test SA VAT Item") + make_item("_Test SA VAT Zero Rated Item", properties = {"is_zero_rated": 1}) + + make_customer() + make_supplier() + + make_sales_invoices() + create_purchase_invoices() + + def tearDown(self): + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company SA VAT'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company SA VAT'") + + def test_vat_audit_report(self): + filters = { + "company": "_Test Company SA VAT", + "from_date": today(), + "to_date": today() + } + columns, data = execute(filters) + total_tax_amount = 0 + total_row_tax = 0 + for row in data: + keys = row.keys() + # skips total row tax_amount in if.. and skips section header in elif.. + if 'voucher_no' in keys: + total_tax_amount = total_tax_amount + row['tax_amount'] + elif 'tax_amount' in keys: + total_row_tax = total_row_tax + row['tax_amount'] + + self.assertEqual(total_tax_amount, total_row_tax) + +def make_company(company_name, abbr): + if not frappe.db.exists("Company", company_name): + company = frappe.get_doc({ + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "ZAR", + "country": "South Africa", + "create_chart_of_accounts_based_on": "Standard Template" + }) + company.insert() + else: + company = frappe.get_doc("Company", company_name) + + company.create_default_warehouses() + + if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): + company.create_default_cost_center() + + company.save() + + return company + +def set_sa_vat_accounts(): + if not frappe.db.exists("South Africa VAT Settings", "_Test Company SA VAT"): + vat_accounts = frappe.get_all( + "Account", + fields=["name"], + filters = { + "company": "_Test Company SA VAT", + "is_group": 0, + "account_type": "Tax" + } + ) + + sa_vat_accounts = [] + for account in vat_accounts: + sa_vat_accounts.append({ + "doctype": "South Africa VAT Account", + "account": account.name + }) + + frappe.get_doc({ + "company": "_Test Company SA VAT", + "vat_accounts": sa_vat_accounts, + "doctype": "South Africa VAT Settings", + }).insert() + +def make_customer(): + if not frappe.db.exists("Customer", "_Test SA Customer"): + frappe.get_doc({ + "doctype": "Customer", + "customer_name": "_Test SA Customer", + "customer_type": "Company", + }).insert() + +def make_supplier(): + if not frappe.db.exists("Supplier", "_Test SA Supplier"): + frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": "_Test SA Supplier", + "supplier_type": "Company", + "supplier_group":"All Supplier Groups" + }).insert() + +def make_item(item_code, properties=None): + if not frappe.db.exists("Item", item_code): + item = frappe.get_doc({ + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products" + }) + + if properties: + item.update(properties) + + item.insert() + +def make_sales_invoices(): + def make_sales_invoices_wrapper(item, rate, tax_account, tax_rate, tax=True): + si = create_sales_invoice( + company="_Test Company SA VAT", + customer = "_Test SA Customer", + currency = "ZAR", + item=item, + rate=rate, + warehouse = "Finished Goods - _TCSV", + debit_to = "Debtors - _TCSV", + income_account = "Sales - _TCSV", + expense_account = "Cost of Goods Sold - _TCSV", + cost_center = "Main - _TCSV", + do_not_save=1 + ) + if tax: + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": tax_account, + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": tax_rate + }) + + si.submit() + + test_item = "_Test SA VAT Item" + test_zero_rated_item = "_Test SA VAT Zero Rated Item" + + make_sales_invoices_wrapper(test_item, 100.0, "VAT - 15% - _TCSV", 15.0) + make_sales_invoices_wrapper(test_zero_rated_item, 100.0, "VAT - 0% - _TCSV", 0.0) + +def create_purchase_invoices(): + pi = make_purchase_invoice( + company = "_Test Company SA VAT", + supplier = "_Test SA Supplier", + supplier_warehouse = "Finished Goods - _TCSV", + warehouse = "Finished Goods - _TCSV", + currency = "ZAR", + cost_center = "Main - _TCSV", + expense_account = "Cost of Goods Sold - _TCSV", + item = "_Test SA VAT Item", + qty = 1, + rate = 100, + uom = "Nos", + do_not_save = 1 + ) + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT - 15% - _TCSV", + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": 15.0 + }) + + pi.submit() diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 292605ef13d..ebf297113d7 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -1,11 +1,11 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals import frappe import json from frappe import _ -from frappe.utils import formatdate +from frappe.utils import formatdate, get_link_to_form def execute(filters=None): return VATAuditReport(filters).run() @@ -42,7 +42,8 @@ class VATAuditReport(object): self.sa_vat_accounts = frappe.get_list("South Africa VAT Account", filters = {"parent": self.filters.company}, pluck="account") if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: - frappe.throw(_("Please set VAT Accounts in South Africa VAT Settings")) + link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") + frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings)) def get_invoice_data(self, doctype): conditions = self.get_conditions() @@ -69,7 +70,7 @@ class VATAuditReport(object): items = frappe.db.sql(""" SELECT - item_code, parent, taxable_value, base_net_amount, is_zero_rated + item_code, parent, base_net_amount, is_zero_rated FROM `tab%s Item` WHERE @@ -79,7 +80,7 @@ class VATAuditReport(object): if d.item_code not in self.invoice_items.get(d.parent, {}): self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, { 'net_amount': 0.0}) - self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('base_net_amount', 0) self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated def get_items_based_on_tax_rate(self, doctype): From c3b1376517df9a693db2e1976ed042ec305d2f5f Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 26 Aug 2021 14:36:42 +0530 Subject: [PATCH 45/48] fix: allow to change incoming rate manually in case of stand-alone credit note (#27164) * fix: allow to change rate manually in case of stand-alone credit note (#27036) Co-authored-by: Marica (cherry picked from commit fe4540d74d0dfda170c2a781347d745fb9f86fb6) # Conflicts: # erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json * fix: resolve conflicts Co-authored-by: rohitwaghchaure Co-authored-by: Ankush Menat --- .../sales_invoice/test_sales_invoice.py | 15 ++++++++- .../sales_invoice_item.json | 12 +++---- .../controllers/sales_and_purchase_return.py | 32 +++++++++++-------- erpnext/controllers/selling_controller.py | 27 ++++++++-------- erpnext/stock/stock_ledger.py | 1 + 5 files changed, 53 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d008b2ae24f..0608437b6e3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1107,6 +1107,18 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + def test_incoming_rate_for_stand_alone_credit_note(self): + return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10, + company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1', + income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1') + + incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate') + debit_amount = frappe.db.get_value('GL Entry', + {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit') + + self.assertEqual(debit_amount, 10.0) + self.assertEqual(incoming_rate, 10.0) + def test_discount_on_net_total(self): si = frappe.copy_doc(test_records[2]) si.apply_discount_on = "Net Total" @@ -2327,7 +2339,8 @@ def create_sales_invoice(**args): "discount_amount": args.discount_amount or 0, "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, - "conversion_factor": 1 + "conversion_factor": 1, + "incoming_rate": args.incoming_rate or 0 }) if not args.do_not_save: diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index eede3268365..d27a3a779ed 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -53,7 +53,6 @@ "column_break_24", "base_net_rate", "base_net_amount", - "incoming_rate", "drop_ship", "delivered_by_supplier", "accounting", @@ -81,6 +80,7 @@ "target_warehouse", "quality_inspection", "batch_no", + "incoming_rate", "col_break5", "allow_zero_valuation_rate", "serial_no", @@ -808,12 +808,12 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_return && parent.update_stock && !parent.return_against", "fieldname": "incoming_rate", "fieldtype": "Currency", - "label": "Incoming Rate", + "label": "Incoming Rate (Costing)", "no_copy": 1, - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "depends_on": "eval: doc.uom != doc.stock_uom", @@ -834,7 +834,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-12 20:15:42.668399", + "modified": "2021-08-19 13:41:53.435827", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -842,4 +842,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5ee1f2f7fb5..01486fcd65d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -394,19 +394,6 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None if not return_against: return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against") - if not return_against and voucher_type == 'Sales Invoice' and sle: - return get_incoming_rate({ - "item_code": sle.item_code, - "warehouse": sle.warehouse, - "posting_date": sle.get('posting_date'), - "posting_time": sle.get('posting_time'), - "qty": sle.actual_qty, - "serial_no": sle.get('serial_no'), - "company": sle.company, - "voucher_type": sle.voucher_type, - "voucher_no": sle.voucher_no - }, raise_error_if_no_rate=False) - return_against_item_field = get_return_against_item_fields(voucher_type) filters = get_filters(voucher_type, voucher_no, voucher_detail_no, @@ -417,7 +404,24 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None else: select_field = "abs(stock_value_difference / actual_qty)" - return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) + rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) + if not (rate and return_against) and voucher_type in ['Sales Invoice', 'Delivery Note']: + rate = frappe.db.get_value(f'{voucher_type} Item', voucher_detail_no, 'incoming_rate') + + if not rate and sle: + rate = get_incoming_rate({ + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.get('posting_date'), + "posting_time": sle.get('posting_time'), + "qty": sle.actual_qty, + "serial_no": sle.get('serial_no'), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no + }, raise_error_if_no_rate=False) + + return rate def get_return_against_item_fields(voucher_type): return_against_item_fields = { diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index fc2cc97e0a5..4ea0e114b48 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -362,7 +362,7 @@ class SellingController(StockController): sales_order.update_reserved_qty(so_item_rows) def set_incoming_rate(self): - if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"): + if self.doctype not in ("Delivery Note", "Sales Invoice"): return items = self.get("items") + (self.get("packed_items") or []) @@ -371,18 +371,19 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get('stock_qty') or d.get('actual_qty')) - d.incoming_rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": self.get('posting_date') or self.get('transaction_date'), - "posting_time": self.get('posting_time') or nowtime(), - "qty": qty if cint(self.get("is_return")) else (-1 * qty), - "serial_no": d.get('serial_no'), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - "allow_zero_valuation": d.get("allow_zero_valuation") - }, raise_error_if_no_rate=False) + if not d.incoming_rate: + d.incoming_rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": self.get('posting_date') or self.get('transaction_date'), + "posting_time": self.get('posting_time') or nowtime(), + "qty": qty if cint(self.get("is_return")) else (-1 * qty), + "serial_no": d.get('serial_no'), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f762cc7b890..b09b663c254 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -324,6 +324,7 @@ class update_entries_after(object): where item_code = %(item_code)s and warehouse = %(warehouse)s + and is_cancelled = 0 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) order by From d12fe7949420a2943eb72abad0d8ff92f5cc8d73 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 26 Aug 2021 14:59:21 +0530 Subject: [PATCH 46/48] fix: Fee Validity fixes (#27161) * fix: Fee Validity fixes (#27156) * chore: update Fee Validity form labels * fix: first appointment should not be considered for Fee Validity * fix: Fee Validity test cases * fix: appointment test case (cherry picked from commit 642b4c805cdf912fdc07de5b998df70091a8c8ac) * fix: overlapping appointments Co-authored-by: Rucha Mahabal --- .../doctype/fee_validity/fee_validity.json | 6 +++--- .../doctype/fee_validity/fee_validity.py | 14 ++------------ .../doctype/fee_validity/test_fee_validity.py | 4 ++-- .../patient_appointment/patient_appointment.py | 6 +++++- .../test_patient_appointment.py | 11 +++++++---- 5 files changed, 19 insertions(+), 22 deletions(-) diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.json b/erpnext/healthcare/doctype/fee_validity/fee_validity.json index b001bf024ce..d76b42e6836 100644 --- a/erpnext/healthcare/doctype/fee_validity/fee_validity.json +++ b/erpnext/healthcare/doctype/fee_validity/fee_validity.json @@ -46,13 +46,13 @@ { "fieldname": "visited", "fieldtype": "Int", - "label": "Visited yet", + "label": "Visits Completed", "read_only": 1 }, { "fieldname": "valid_till", "fieldtype": "Date", - "label": "Valid till", + "label": "Valid Till", "read_only": 1 }, { @@ -106,7 +106,7 @@ ], "in_create": 1, "links": [], - "modified": "2020-03-17 20:25:06.487418", + "modified": "2021-08-26 10:51:05.609349", "modified_by": "Administrator", "module": "Healthcare", "name": "Fee Validity", diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.py b/erpnext/healthcare/doctype/fee_validity/fee_validity.py index 5b9c17934fa..59586e0c31b 100644 --- a/erpnext/healthcare/doctype/fee_validity/fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/fee_validity.py @@ -11,7 +11,6 @@ import datetime class FeeValidity(Document): def validate(self): self.update_status() - self.set_start_date() def update_status(self): if self.visited >= self.max_visits: @@ -19,13 +18,6 @@ class FeeValidity(Document): else: self.status = 'Pending' - def set_start_date(self): - self.start_date = getdate() - for appointment in self.ref_appointments: - appointment_date = frappe.db.get_value('Patient Appointment', appointment.appointment, 'appointment_date') - if getdate(appointment_date) < self.start_date: - self.start_date = getdate(appointment_date) - def create_fee_validity(appointment): if not check_is_new_patient(appointment): @@ -36,11 +28,9 @@ def create_fee_validity(appointment): fee_validity.patient = appointment.patient fee_validity.max_visits = frappe.db.get_single_value('Healthcare Settings', 'max_visits') or 1 valid_days = frappe.db.get_single_value('Healthcare Settings', 'valid_days') or 1 - fee_validity.visited = 1 + fee_validity.visited = 0 + fee_validity.start_date = getdate(appointment.appointment_date) fee_validity.valid_till = getdate(appointment.appointment_date) + datetime.timedelta(days=int(valid_days)) - fee_validity.append('ref_appointments', { - 'appointment': appointment.name - }) fee_validity.save(ignore_permissions=True) return fee_validity diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 29b4c5c9b98..957f85211de 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -22,14 +22,14 @@ class TestFeeValidity(unittest.TestCase): item = create_healthcare_service_items() healthcare_settings = frappe.get_single("Healthcare Settings") healthcare_settings.enable_free_follow_ups = 1 - healthcare_settings.max_visits = 2 + healthcare_settings.max_visits = 1 healthcare_settings.valid_days = 7 healthcare_settings.automate_appointment_invoicing = 1 healthcare_settings.op_consulting_charge_item = item healthcare_settings.save(ignore_permissions=True) patient, practitioner = create_healthcare_docs() - # For first appointment, invoice is generated + # For first appointment, invoice is generated. First appointment not considered in fee validity appointment = create_appointment(patient, practitioner, nowdate()) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") self.assertEqual(invoiced, 1) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 10f2d537891..36047c48381 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -137,9 +137,13 @@ class PatientAppointment(Document): frappe.db.set_value('Patient Appointment', self.name, 'notes', comments) def update_fee_validity(self): + if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): + return + fee_validity = manage_fee_validity(self) if fee_validity: - frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) + frappe.msgprint(_('{0}: {1} has fee validity till {2}').format(self.patient, + frappe.bold(self.patient_name), fee_validity.valid_till)) @frappe.whitelist() def get_therapy_types(self): diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 062a32a92e6..d0db3226326 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -110,18 +110,21 @@ class TestPatientAppointment(unittest.TestCase): patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) appointment = create_appointment(patient, practitioner, nowdate()) - fee_validity = frappe.db.get_value('Fee Validity Reference', {'appointment': appointment.name}, 'parent') + fee_validity = frappe.db.get_value('Fee Validity', {'patient': patient, 'practitioner': practitioner}) # fee validity created self.assertTrue(fee_validity) - visited = frappe.db.get_value('Fee Validity', fee_validity, 'visited') + # first follow up appointment + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1)) + self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 1) + update_status(appointment.name, 'Cancelled') # check fee validity updated - self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), visited - 1) + self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 0) frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1) update_status(appointment.name, 'Cancelled') # check invoice cancelled sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') From 55ff6277a7d1ef8d677dc42a14f2cecc9bef60b0 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 26 Aug 2021 15:42:23 +0530 Subject: [PATCH 47/48] fix: Merge conflicts and place internal customer creation util in test_customer.py --- .../sales_invoice/test_sales_invoice.py | 114 +----------------- .../selling/doctype/customer/test_customer.py | 24 ++-- .../delivery_note/test_delivery_note.py | 3 +- 3 files changed, 17 insertions(+), 124 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index bb98cf33203..6c1fc3f3a03 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1787,7 +1787,8 @@ class TestSalesInvoice(unittest.TestCase): create_internal_customer( customer_name="_Test Internal Customer", - represents_company="_Test Company 1" + represents_company="_Test Company 1", + allowed_to_interact_with="Wind Power LLC" ) if not frappe.db.exists("Supplier", "_Test Internal Supplier"): @@ -1831,91 +1832,6 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") -<<<<<<< HEAD -======= - def test_inter_company_transaction_without_default_warehouse(self): - "Check mapping (expense account) of inter company SI to PI in absence of default warehouse." - # setup - old_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - - old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled('_Test Company 1') - frappe.local.enable_perpetual_inventory['_Test Company 1'] = 1 - - frappe.db.set_value("Company", '_Test Company 1', "stock_received_but_not_billed", "Stock Received But Not Billed - _TC1") - frappe.db.set_value("Company", '_Test Company 1', "expenses_included_in_valuation", "Expenses Included In Valuation - _TC1") - - - if not frappe.db.exists("Customer", "_Test Internal Customer"): - customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Internal Customer", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": "_Test Company 1" - }) - - customer.append("companies", { - "company": "Wind Power LLC" - }) - - customer.insert() - - if not frappe.db.exists("Supplier", "_Test Internal Supplier"): - supplier = frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Internal Supplier", - "doctype": "Supplier", - "is_internal_supplier": 1, - "represents_company": "Wind Power LLC" - }) - - supplier.append("companies", { - "company": "_Test Company 1" - }) - - supplier.insert() - - # begin test - si = create_sales_invoice( - company = "Wind Power LLC", - customer = "_Test Internal Customer", - debit_to = "Debtors - WP", - warehouse = "Stores - WP", - income_account = "Sales - WP", - expense_account = "Cost of Goods Sold - WP", - cost_center = "Main - WP", - currency = "USD", - update_stock = 1, - do_not_save = 1 - ) - si.selling_price_list = "_Test Price List Rest of the World" - si.submit() - - target_doc = make_inter_company_transaction("Sales Invoice", si.name) - - # in absence of warehouse Stock Received But Not Billed is set as expense account while mapping - # mapping is not obstructed - self.assertIsNone(target_doc.items[0].warehouse) - self.assertEqual(target_doc.items[0].expense_account, "Stock Received But Not Billed - _TC1") - - target_doc.items[0].update({"cost_center": "Main - _TC1"}) - - # missing warehouse is validated on save, after mapping - self.assertRaises(WarehouseMissingError, target_doc.save) - - target_doc.items[0].update({"warehouse": "Stores - _TC1"}) - target_doc.save() - - # after warehouse is set, linked account or default inventory account is set - self.assertEqual(target_doc.items[0].expense_account, 'Stock In Hand - _TC1') - - # tear down - frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock) - def test_sle_if_target_warehouse_exists_accidentally(self): """ Check if inward entry exists if Target Warehouse accidentally exists @@ -1949,9 +1865,10 @@ class TestSalesInvoice(unittest.TestCase): si.cancel() se.cancel() ->>>>>>> f4dc9ee2aa (fix: Don't create inward SLE against SI unless is internal customer enabled (#27086)) def test_internal_transfer_gl_entry(self): ## Create internal transfer account + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + account = create_account(account_name="Unrealized Profit", parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") @@ -2488,29 +2405,6 @@ def get_taxes_and_charges(): "row_id": 1 }] -def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): - if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": represents_company - }) - - customer.append("companies", { - "company": allowed_to_interact_with - }) - - customer.insert() - customer_name = customer.name - else: - customer_name = frappe.db.get_value("Customer", customer_name) - - return customer_name - def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Supplier", supplier_name): supplier = frappe.get_doc({ diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 908ba270b04..5b337313d3d 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -353,27 +353,25 @@ def set_credit_limit(customer, company, credit_limit): }) customer.credit_limits[-1].db_insert() -def create_internal_customer(**args): - args = frappe._dict(args) - - customer_name = args.get("customer_name") or "_Test Internal Customer" - +def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Customer", customer_name): customer = frappe.get_doc({ "doctype": "Customer", - "customer_group": args.customer_group or "_Test Customer Group", + "customer_group": "_Test Customer Group", "customer_name": customer_name, - "customer_type": args.customer_type or "Individual", - "territory": args.territory or "_Test Territory", + "customer_type": "Individual", + "territory": "_Test Territory", "is_internal_customer": 1, - "represents_company": args.represents_company or "_Test Company with perpetual inventory" + "represents_company": represents_company }) customer.append("companies", { - "company": args.allowed_company or "Wind Power LLC" + "company": allowed_to_interact_with }) - customer.insert() - return customer + customer.insert() + customer_name = customer.name else: - return frappe.get_cached_doc("Customer", customer_name) \ No newline at end of file + customer_name = frappe.db.get_value("Customer", customer_name) + + return customer_name \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index dffc73020d6..e6736b2bab3 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -435,7 +435,8 @@ class TestDeliveryNote(unittest.TestCase): company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') customer = create_internal_customer( customer_name="_Test Internal Customer 2", - allowed_company="_Test Company with perpetual inventory" + represents_company="_Test Company with perpetual inventory", + allowed_to_interact_with="_Test Company with perpetual inventory" ) set_valuation_method("_Test Item", "FIFO") From 614ee71778c25eebdc5a7d3271a66f5b5ec176e5 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 26 Aug 2021 16:21:48 +0530 Subject: [PATCH 48/48] fix: internal customer util returns 'str' not doc object --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index e6736b2bab3..b333a6b57ea 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -433,7 +433,7 @@ class TestDeliveryNote(unittest.TestCase): from erpnext.selling.doctype.customer.test_customer import create_internal_customer company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - customer = create_internal_customer( + customer_name = create_internal_customer( customer_name="_Test Internal Customer 2", represents_company="_Test Company with perpetual inventory", allowed_to_interact_with="_Test Company with perpetual inventory" @@ -454,7 +454,7 @@ class TestDeliveryNote(unittest.TestCase): dn = create_delivery_note( item_code="_Test Product Bundle Item", company="_Test Company with perpetual inventory", - customer=customer.name, + customer=customer_name, cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True,