From 59239172a1ed352848fe06c705a68281c41e3106 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Sep 2020 17:34:21 +0530 Subject: [PATCH 01/37] feat: Supplier Quotation Comparison - v3 --- .../quoted_item_comparison.json | 32 ------- .../__init__.py | 0 .../supplier_quotation_comparison.html} | 0 .../supplier_quotation_comparison.js} | 12 ++- .../supplier_quotation_comparison.json | 32 +++++++ .../supplier_quotation_comparison.py} | 83 ++++++++++++++----- 6 files changed, 106 insertions(+), 53 deletions(-) delete mode 100644 erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.json rename erpnext/buying/report/{quoted_item_comparison => supplier_quotation_comparison}/__init__.py (100%) rename erpnext/buying/report/{quoted_item_comparison/quoted_item_comparison.html => supplier_quotation_comparison/supplier_quotation_comparison.html} (100%) rename erpnext/buying/report/{quoted_item_comparison/quoted_item_comparison.js => supplier_quotation_comparison/supplier_quotation_comparison.js} (91%) create mode 100644 erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.json rename erpnext/buying/report/{quoted_item_comparison/quoted_item_comparison.py => supplier_quotation_comparison/supplier_quotation_comparison.py} (70%) diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.json b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.json deleted file mode 100644 index 23b3ace49c..0000000000 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2016-07-21 08:31:05.890362", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 20:04:58.784351", - "modified_by": "Administrator", - "module": "Buying", - "name": "Quoted Item Comparison", - "owner": "Administrator", - "ref_doctype": "Supplier Quotation", - "report_name": "Quoted Item Comparison", - "report_type": "Script Report", - "roles": [ - { - "role": "Manufacturing Manager" - }, - { - "role": "Purchase Manager" - }, - { - "role": "Purchase User" - }, - { - "role": "Stock User" - } - ] -} \ No newline at end of file diff --git a/erpnext/buying/report/quoted_item_comparison/__init__.py b/erpnext/buying/report/supplier_quotation_comparison/__init__.py similarity index 100% rename from erpnext/buying/report/quoted_item_comparison/__init__.py rename to erpnext/buying/report/supplier_quotation_comparison/__init__.py diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.html b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html similarity index 100% rename from erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.html rename to erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js similarity index 91% rename from erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js rename to erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index 518d665e7e..80e521a8bf 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -1,7 +1,7 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.query_reports["Quoted Item Comparison"] = { +frappe.query_reports["Supplier Quotation Comparison"] = { filters: [ { fieldtype: "Link", @@ -78,6 +78,13 @@ frappe.query_reports["Quoted Item Comparison"] = { return { filters: { "docstatus": ["<", 2] } } } }, + { + "fieldname":"group_by", + "label": __("Group by"), + "fieldtype": "Select", + "options": [__("Group by Supplier"), __("Group by Item")], + "default": __("Group by Supplier") + }, { fieldtype: "Check", label: __("Include Expired"), @@ -98,6 +105,9 @@ frappe.query_reports["Quoted Item Comparison"] = { } } + if(column.fieldname === "price_per_unit" && data.price_per_unit && data.min && data.min === 1){ + value = `
${value}
`; + } return value; }, diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.json b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.json new file mode 100644 index 0000000000..886e5b8757 --- /dev/null +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2016-07-21 08:31:05.890362", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 2, + "is_standard": "Yes", + "modified": "2017-02-24 20:04:58.784351", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier Quotation Comparison", + "owner": "Administrator", + "ref_doctype": "Supplier Quotation", + "report_name": "Supplier Quotation Comparison", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing Manager" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py similarity index 70% rename from erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py rename to erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 4426560c16..d2399b1ec9 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -12,9 +12,9 @@ def execute(filters=None): if not filters: return [], [] + columns = get_columns(filters) conditions = get_conditions(filters) supplier_quotation_data = get_data(filters, conditions) - columns = get_columns() data, chart_data = prepare_data(supplier_quotation_data, filters) message = get_message() @@ -41,9 +41,13 @@ def get_conditions(filters): return conditions def get_data(filters, conditions): - supplier_quotation_data = frappe.db.sql("""SELECT - sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, - sqi.lead_time_days, sq.supplier, sq.valid_till + supplier_quotation_data = frappe.db.sql(""" + SELECT + sqi.parent, sqi.item_code, + sqi.qty, sqi.stock_qty, sqi.rate, + sqi.uom, sqi.stock_uom, + sqi.request_for_quotation, + sqi.lead_time_days, sq.supplier as supplier_name, sq.valid_till FROM `tabSupplier Quotation Item` sqi, `tabSupplier Quotation` sq @@ -58,16 +62,18 @@ def get_data(filters, conditions): return supplier_quotation_data def prepare_data(supplier_quotation_data, filters): - out, suppliers, qty_list, chart_data = [], [], [], [] - supplier_wise_map = defaultdict(list) + out, groups, qty_list, suppliers, chart_data = [], [], [], [], [] + group_wise_map = defaultdict(list) supplier_qty_price_map = {} + group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code" company_currency = frappe.db.get_default("currency") float_precision = cint(frappe.db.get_default("float_precision")) or 2 for data in supplier_quotation_data: - supplier = data.get("supplier") - supplier_currency = frappe.db.get_value("Supplier", data.get("supplier"), "default_currency") + group = data.get(group_by_field) # get item or supplier value for this row + + supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency") if supplier_currency: exchange_rate = get_exchange_rate(supplier_currency, company_currency) @@ -75,38 +81,55 @@ def prepare_data(supplier_quotation_data, filters): exchange_rate = 1 row = { - "item_code": data.get('item_code'), + "item_code": "" if group_by_field=="item_code" else data.get("item_code"), # leave blank if group by field + "supplier_name": "" if group_by_field=="supplier_name" else data.get("supplier_name"), "quotation": data.get("parent"), "qty": data.get("qty"), "price": flt(data.get("rate") * exchange_rate, float_precision), "uom": data.get("uom"), + "stock_uom": data.get('stock_uom'), "request_for_quotation": data.get("request_for_quotation"), "valid_till": data.get('valid_till'), "lead_time_days": data.get('lead_time_days') } + row["price_per_unit"] = flt(row["price"]) / (flt(data.get("stock_qty")) or 1) - # map for report view of form {'supplier1':[{},{},...]} - supplier_wise_map[supplier].append(row) + # map for report view of form {'supplier1'/'item1':[{},{},...]} + group_wise_map[group].append(row) # map for chart preparation of the form {'supplier1': {'qty': 'price'}} + supplier = data.get("supplier_name") if filters.get("item_code"): if not supplier in supplier_qty_price_map: supplier_qty_price_map[supplier] = {} supplier_qty_price_map[supplier][row["qty"]] = row["price"] + groups.append(group) suppliers.append(supplier) qty_list.append(data.get("qty")) + groups = list(set(groups)) suppliers = list(set(suppliers)) qty_list = list(set(qty_list)) + highlight_min_price = group_by_field == "item_code" + # final data format for report view - for supplier in suppliers: - supplier_wise_map[supplier][0].update({"supplier_name": supplier}) - for entry in supplier_wise_map[supplier]: + for group in groups: + group_entries = group_wise_map[group] # all entries pertaining to item/supplier + group_entries[0].update({group_by_field : group}) + + if highlight_min_price: + prices = [group_entry["price_per_unit"] for group_entry in group_entries] + min_price = min(prices) + + for entry in group_entries: + if highlight_min_price and entry["price_per_unit"] == min_price: + entry["min"] = 1 out.append(entry) if filters.get("item_code"): + # render chart only for one item comparison chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) return out, chart_data @@ -145,8 +168,9 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): return chart_data -def get_columns(): - columns = [{ +def get_columns(filters): + group_by_columns = [ + { "fieldname": "supplier_name", "label": _("Supplier"), "fieldtype": "Link", @@ -158,8 +182,10 @@ def get_columns(): "label": _("Item"), "fieldtype": "Link", "options": "Item", - "width": 200 - }, + "width": 150 + }] + + columns = [ { "fieldname": "uom", "label": _("UOM"), @@ -180,6 +206,20 @@ def get_columns(): "options": "Company:company:default_currency", "width": 110 }, + { + "fieldname": "stock_uom", + "label": _("Stock UOM"), + "fieldtype": "Link", + "options": "UOM", + "width": 90 + }, + { + "fieldname": "price_per_unit", + "label": _("Price per Unit (Stock UOM)"), + "fieldtype": "Currency", + "options": "Company:company:default_currency", + "width": 120 + }, { "fieldname": "quotation", "label": _("Supplier Quotation"), @@ -205,9 +245,12 @@ def get_columns(): "fieldtype": "Link", "options": "Request for Quotation", "width": 150 - } - ] + }] + if filters.get("group_by") == "Group by Item": + group_by_columns.reverse() + + columns[0:0] = group_by_columns # add positioned group by columns to the report return columns def get_message(): From 90d9d80ef98fffaa17e4511a9b076d723a5b6fe3 Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 19 Sep 2020 00:43:53 +0530 Subject: [PATCH 02/37] fix: More MR UX fixes --- .../production_plan/production_plan.py | 1 - .../doctype/sales_order/sales_order.py | 1 - .../material_request/material_request.js | 3 +- .../material_request/material_request.json | 47 +++++---------- .../material_request_dashboard.py | 2 +- .../material_request_item.json | 59 ++++++++----------- 6 files changed, 44 insertions(+), 69 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c8892376b7..5b14d054af 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -381,7 +381,6 @@ class ProductionPlan(Document): "transaction_date": nowdate(), "status": "Draft", "company": self.company, - "requested_by": frappe.session.user, 'material_request_type': material_request_type, 'customer': item_doc.customer or '' }) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index f88289871e..62a5d4e68f 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -989,7 +989,6 @@ def make_raw_material_request(items, company, sales_order, project=None): doctype = 'Material Request', transaction_date = nowdate(), company = company, - requested_by = frappe.session.user, material_request_type = 'Purchase' )) for item in raw_materials: diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 3c4e35349e..8c470988c8 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -280,7 +280,8 @@ frappe.ui.form.on('Material Request', { fieldname:'default_supplier', fieldtype: 'Link', options: 'Supplier', - description: __('Select a Supplier from the Default Supplier List of the items below.'), + description: __('Select a Supplier from the Default Suppliers of the items below. \ + On selection, a Purchase Order will be made against items belonging to the selected Supplier only.'), get_query: () => { return{ query: "erpnext.stock.doctype.material_request.material_request.get_default_supplier_query", diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 44503d22a3..da73bc8015 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -11,9 +11,10 @@ "naming_series", "title", "material_request_type", - "transfer_status", "customer", + "status", "column_break_2", + "transaction_date", "schedule_date", "company", "amended_from", @@ -25,11 +26,8 @@ "scan_barcode", "items", "more_info", - "requested_by", - "transaction_date", - "column_break2", - "status", "per_ordered", + "column_break2", "per_received", "printing_details", "letter_head", @@ -82,7 +80,8 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer" + "options": "Customer", + "print_hide": 1 }, { "fieldname": "column_break_2", @@ -92,12 +91,12 @@ "allow_on_submit": 1, "fieldname": "schedule_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Required By" }, { "fieldname": "company", "fieldtype": "Link", - "in_list_view": 1, "in_standard_filter": 1, "label": "Company", "oldfieldname": "company", @@ -153,18 +152,10 @@ "oldfieldtype": "Section Break", "options": "fa fa-file-text" }, - { - "fieldname": "requested_by", - "fieldtype": "Data", - "in_standard_filter": 1, - "label": "Requested For", - "options": "Email" - }, { "default": "Today", "fieldname": "transaction_date", "fieldtype": "Date", - "in_list_view": 1, "label": "Transaction Date", "no_copy": 1, "oldfieldname": "transaction_date", @@ -197,7 +188,7 @@ "width": "100px" }, { - "depends_on": "eval:doc.docstatus==1", + "depends_on": "eval:doc.per_ordered > 0", "fieldname": "per_ordered", "fieldtype": "Percent", "label": "% Ordered", @@ -208,7 +199,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.docstatus==1", + "depends_on": "eval:doc.per_received > 0", "fieldname": "per_received", "fieldtype": "Percent", "label": "% Received", @@ -282,13 +273,15 @@ }, { "fieldname": "warehouse_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Warehouse" }, { - "description": "Sets 'For Warehouse' in each row of the Items table.", + "description": "Sets 'Target Warehouse' in each row of the Items table.", "fieldname": "set_warehouse", "fieldtype": "Link", - "label": "Set Warehouse", + "in_list_view": 1, + "label": "Set Target Warehouse", "options": "Warehouse" }, { @@ -300,26 +293,18 @@ }, { "depends_on": "eval:doc.material_request_type == 'Material Transfer'", + "description": "Sets 'Source Warehouse' in each row of the Items table.", "fieldname": "set_from_warehouse", "fieldtype": "Link", - "label": "Set From Warehouse", + "label": "Set Source Warehouse", "options": "Warehouse" - }, - { - "allow_on_submit": 1, - "depends_on": "eval:doc.add_to_transit == 1", - "fieldname": "transfer_status", - "fieldtype": "Select", - "label": "Transfer Status", - "options": "\nNot Started\nIn Transit\nCompleted", - "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2020-08-10 13:27:54.891058", + "modified": "2020-09-19 00:36:00.306761", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index 0e4fb7a6dd..f3e5e5db25 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -7,7 +7,7 @@ def get_data(): 'fieldname': 'material_request', 'transactions': [ { - 'label': _('Related'), + 'label': _('Reference'), 'items': ['Request for Quotation', 'Supplier Quotation', 'Purchase Order'] }, { diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 32bd4a0a57..08c273838f 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -13,12 +13,10 @@ "schedule_date", "section_break_4", "description", + "column_break_12", "item_group", "brand", - "image_section", "image", - "column_break_12", - "manufacturer_part_no", "quantity_and_warehouse", "qty", "stock_uom", @@ -34,26 +32,26 @@ "amount", "manufacture_details", "manufacturer", + "manufacturer_part_no", + "col_break_mfg", + "bom_no", + "accounting_dimensions_section", + "project", + "dimension_col_break", + "cost_center", "more_info", "lead_time_date", "sales_order", "sales_order_item", "production_plan", "material_request_plan_item", + "expense_account", "col_break4", "min_order_qty", "projected_qty", "actual_qty", "ordered_qty", "received_qty", - "accounting_details", - "expense_account", - "accounting_dimensions_section", - "project", - "dimension_col_break", - "cost_center", - "section_break_37", - "bom_no", "section_break_46", "page_break" ], @@ -164,7 +162,7 @@ "fieldname": "warehouse", "fieldtype": "Link", "in_list_view": 1, - "label": "For Warehouse", + "label": "Target Warehouse", "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", @@ -191,12 +189,14 @@ { "fieldname": "rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Rate", + "print_hide": 1 }, { "fieldname": "amount", "fieldtype": "Currency", "label": "Amount", + "print_hide": 1, "read_only": 1 }, { @@ -326,6 +326,7 @@ "report_hide": 1 }, { + "depends_on": "eval:doc.docstatus==1", "fieldname": "ordered_qty", "fieldtype": "Float", "label": "Completed Qty", @@ -335,12 +336,6 @@ "print_hide": 1, "read_only": 1 }, - { - "collapsible": 1, - "fieldname": "accounting_details", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, { "fieldname": "expense_account", "fieldtype": "Link", @@ -367,21 +362,10 @@ "print_hide": 1 }, { - "collapsible": 1, - "fieldname": "image_section", - "fieldtype": "Section Break", - "label": "Image" - }, - { - "depends_on": "eval:parent.material_request_type == \"Manufacture\"", - "fieldname": "section_break_37", - "fieldtype": "Section Break", - "label": "Manufacturing" - }, - { + "depends_on": "eval:doc.docstatus==1", "fieldname": "received_qty", "fieldtype": "Float", - "label": "Received Quantity", + "label": "Received Qty", "no_copy": 1, "print_hide": 1, "read_only": 1 @@ -398,6 +382,7 @@ }, { "collapsible": 1, + "depends_on": "eval:in_list([\"Manufacture\", \"Purchase\"], parent.material_request_type)", "fieldname": "manufacture_details", "fieldtype": "Section Break", "label": "Manufacture" @@ -430,10 +415,11 @@ "depends_on": "eval:parent.material_request_type == \"Material Transfer\"", "fieldname": "from_warehouse", "fieldtype": "Link", - "label": "Source Warehouse (Material Transfer)", + "label": "Source Warehouse", "options": "Warehouse" }, { + "allow_on_submit": 1, "fieldname": "bom_no", "fieldtype": "Link", "label": "BOM No", @@ -444,12 +430,17 @@ { "fieldname": "section_break_46", "fieldtype": "Section Break" + }, + { + "fieldname": "col_break_mfg", + "fieldtype": "Column Break" } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-05-15 09:00:00.992835", + "modified": "2020-09-18 20:52:33.265074", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", From b782d2a99d74576494d38f7abb02762a7b84d98c Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 19 Sep 2020 01:02:34 +0530 Subject: [PATCH 03/37] fix: Added back accidentally deleted field --- .../doctype/material_request/material_request.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index da73bc8015..d73349dd39 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -11,6 +11,7 @@ "naming_series", "title", "material_request_type", + "transfer_status", "customer", "status", "column_break_2", @@ -298,13 +299,22 @@ "fieldtype": "Link", "label": "Set Source Warehouse", "options": "Warehouse" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.add_to_transit == 1", + "fieldname": "transfer_status", + "fieldtype": "Select", + "label": "Transfer Status", + "options": "\nNot Started\nIn Transit\nCompleted", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2020-09-19 00:36:00.306761", + "modified": "2020-09-19 01:04:09.285862", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", From 3aedeb642f8e18dc4f3a7a61f7932beb46ce7bc1 Mon Sep 17 00:00:00 2001 From: michellealva Date: Tue, 29 Sep 2020 12:54:55 +0530 Subject: [PATCH 04/37] fix: Change Error Message in Work Order --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index b7d968e974..3173b6c483 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -434,7 +434,7 @@ class WorkOrder(Document): elif flt(d.completed_qty) <= max_allowed_qty_for_wo: d.status = "Completed" else: - frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) + frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'")) def set_actual_dates(self): if self.get("operations"): From 81ad90c1d466b6927affddf01ba9027dc0e40611 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 29 Sep 2020 11:54:57 +0200 Subject: [PATCH 05/37] feat: validate sales invoice for germany --- .../doctype/sales_invoice/sales_invoice.json | 10 +++- erpnext/hooks.py | 3 + .../regional/germany/accounts_controller.py | 57 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 erpnext/regional/germany/accounts_controller.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 2397b7d0cb..ed1ed4393d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -19,6 +19,7 @@ "is_return", "column_break1", "company", + "company_tax_id", "posting_date", "posting_time", "set_posting_time", @@ -1940,13 +1941,20 @@ "hide_seconds": 1, "label": "Is Internal Customer", "read_only": 1 + }, + { + "fetch_from": "company.tax_id", + "fieldname": "company_tax_id", + "fieldtype": "Data", + "label": "Company Tax ID", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-08-27 01:56:28.532140", + "modified": "2020-09-29 10:00:41.470858", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4e05076a3d..d9a639a395 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -393,6 +393,9 @@ regional_overrides = { 'Italy': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', + }, + 'Germany': { + 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional', } } user_privacy_documents = [ diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py new file mode 100644 index 0000000000..193c8e14a3 --- /dev/null +++ b/erpnext/regional/germany/accounts_controller.py @@ -0,0 +1,57 @@ +import frappe +from frappe import _ +from frappe import msgprint + + +REQUIRED_FIELDS = { + "Sales Invoice": [ + { + "field_name": "company_address", + "regulation": "§ 14 Abs. 4 Nr. 1 UStG" + }, + { + "field_name": "company_tax_id", + "regulation": "§ 14 Abs. 4 Nr. 2 UStG" + }, + { + "field_name": "taxes", + "regulation": "§ 14 Abs. 4 Nr. 8 UStG", + "condition": "not exempt_from_sales_tax" + }, + { + "field_name": "customer_address", + "regulation": "§ 14 Abs. 4 Nr. 1 UStG", + "condition": "base_grand_total > 250" + } + ] +} + + +def validate_regional(doc): + """Check if required fields for this document are present.""" + required_fields = REQUIRED_FIELDS.get(doc.doctype) + if not required_fields: + return + + meta = frappe.get_meta(doc.doctype) + field_map = {field.fieldname: field.label for field in meta.fields} + + for field in required_fields: + condition = field.get("condition") + if condition and not frappe.safe_eval(condition, doc.as_dict()): + continue + + field_name = field.get("field_name") + regulation = field.get("regulation") + if field_name and not doc.get(field_name): + missing(field_map.get(field_name), regulation) + + +def missing(field_label, regulation): + """Notify the user that a required field is missing.""" + context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.' + msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format( + field_label=frappe.bold(_(field_label)), + regulation=regulation + ) + ) From f90c3602a73786e101ae7a2fcf1bfb1e4551b743 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Oct 2020 14:38:17 +0530 Subject: [PATCH 06/37] fix: Price should be total amount, modified deskpage --- erpnext/buying/desk_page/buying/buying.json | 4 ++-- .../supplier_quotation_comparison.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/buying/desk_page/buying/buying.json b/erpnext/buying/desk_page/buying/buying.json index 565d39c3c8..2e870fea82 100644 --- a/erpnext/buying/desk_page/buying/buying.json +++ b/erpnext/buying/desk_page/buying/buying.json @@ -33,7 +33,7 @@ { "hidden": 0, "label": "Other Reports", - "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Items To Be Requested\",\n \"name\": \"Items To Be Requested\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase History\",\n \"name\": \"Item-wise Purchase History\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"reference_doctype\": \"Purchase Receipt\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"reference_doctype\": \"Purchase Invoice\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Quoted Item Comparison\",\n \"name\": \"Quoted Item Comparison\",\n \"onboard\": 1,\n \"reference_doctype\": \"Supplier Quotation\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Material Requests for which Supplier Quotations are not created\",\n \"name\": \"Material Requests for which Supplier Quotations are not created\",\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"reference_doctype\": \"Address\",\n \"route_options\": {\n \"party_type\": \"Supplier\"\n },\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Items To Be Requested\",\n \"name\": \"Items To Be Requested\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase History\",\n \"name\": \"Item-wise Purchase History\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"reference_doctype\": \"Purchase Receipt\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"reference_doctype\": \"Purchase Invoice\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Quotation Comparison\",\n \"name\": \"Supplier Quotation Comparison\",\n \"onboard\": 1,\n \"reference_doctype\": \"Supplier Quotation\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Material Requests for which Supplier Quotations are not created\",\n \"name\": \"Material Requests for which Supplier Quotations are not created\",\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"reference_doctype\": \"Address\",\n \"route_options\": {\n \"party_type\": \"Supplier\"\n },\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -60,7 +60,7 @@ "idx": 0, "is_standard": 1, "label": "Buying", - "modified": "2020-06-29 19:30:24.983050", + "modified": "2020-09-30 14:40:55.638458", "modified_by": "Administrator", "module": "Buying", "name": "Buying", diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index d2399b1ec9..2b371915f3 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -44,7 +44,7 @@ def get_data(filters, conditions): supplier_quotation_data = frappe.db.sql(""" SELECT sqi.parent, sqi.item_code, - sqi.qty, sqi.stock_qty, sqi.rate, + sqi.qty, sqi.stock_qty, sqi.amount, sqi.uom, sqi.stock_uom, sqi.request_for_quotation, sqi.lead_time_days, sq.supplier as supplier_name, sq.valid_till @@ -85,7 +85,7 @@ def prepare_data(supplier_quotation_data, filters): "supplier_name": "" if group_by_field=="supplier_name" else data.get("supplier_name"), "quotation": data.get("parent"), "qty": data.get("qty"), - "price": flt(data.get("rate") * exchange_rate, float_precision), + "price": flt(data.get("amount") * exchange_rate, float_precision), "uom": data.get("uom"), "stock_uom": data.get('stock_uom'), "request_for_quotation": data.get("request_for_quotation"), @@ -112,12 +112,12 @@ def prepare_data(supplier_quotation_data, filters): suppliers = list(set(suppliers)) qty_list = list(set(qty_list)) - highlight_min_price = group_by_field == "item_code" + highlight_min_price = group_by_field == "item_code" or filters.get("item_code") # final data format for report view for group in groups: group_entries = group_wise_map[group] # all entries pertaining to item/supplier - group_entries[0].update({group_by_field : group}) + group_entries[0].update({group_by_field : group}) # Add item/supplier name in first group row if highlight_min_price: prices = [group_entry["price_per_unit"] for group_entry in group_entries] From 4993ef19d6fe4280ea7b95c2e8ee7e1905a90c5d Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Oct 2020 16:35:09 +0530 Subject: [PATCH 07/37] feat: Supplier Quotation UX fixes --- .../request_for_quotation.js | 2 +- .../request_for_quotation.py | 12 ++-- .../supplier_quotation/supplier_quotation.js | 28 +++++--- .../supplier_quotation_item.json | 69 +++++++++++++------ 4 files changed, 75 insertions(+), 36 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 4a937f7f0d..cf5d7cd4c5 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -179,7 +179,7 @@ frappe.ui.form.on("Request for Quotation",{ dialog.hide(); return frappe.call({ type: "GET", - method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation", + method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq", args: { "source_name": doc.name, "for_supplier": args.supplier diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index b54a585b97..fd04b0d2a6 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -214,14 +214,14 @@ def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent limit %(start)s, %(page_len)s""", {"start": start, "page_len":page_len, "txt": "%%%s%%" % txt, "name": filters.get('supplier')}) -# This method is used to make supplier quotation from material request form. @frappe.whitelist() -def make_supplier_quotation(source_name, for_supplier, target_doc=None): +def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None): def postprocess(source, target_doc): - target_doc.supplier = for_supplier - args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) - target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company) - target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list') + if for_supplier: + target_doc.supplier = for_supplier + args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) + target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company) + target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list') set_missing_values(source, target_doc) doclist = get_mapped_doc("Request for Quotation", source_name, { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index 1b8b40459f..4a2a0786f3 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -8,8 +8,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext setup: function() { this.frm.custom_make_buttons = { 'Purchase Order': 'Purchase Order', - 'Quotation': 'Quotation', - 'Subscription': 'Subscription' + 'Quotation': 'Quotation' } this._super(); @@ -28,12 +27,6 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext cur_frm.page.set_inner_btn_group_as_primary(__('Create')); cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __('Create')); - - if(!this.frm.doc.auto_repeat) { - cur_frm.add_custom_button(__('Subscription'), function() { - erpnext.utils.make_subscription(me.frm.doc.doctype, me.frm.doc.name) - }, __('Create')) - } } else if (this.frm.doc.docstatus===0) { @@ -54,6 +47,25 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext } }) }, __("Get items from")); + + this.frm.add_custom_button(__("Request for Quotation"), + function() { + if (!me.frm.doc.supplier) { + frappe.throw({message:__("Please select a Supplier"), title:__("Mandatory")}) + } + erpnext.utils.map_current_doc({ + method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq", + source_doctype: "Request for Quotation", + target: me.frm, + setters: { + company: me.frm.doc.company, + transaction_date: null + }, + get_query_filters: { + docstatus: 1, + } + }) + }, __("Get items from")); } }, diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index b50e834ec7..7d624357f3 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -12,6 +12,8 @@ "item_name", "column_break_3", "lead_time_days", + "expected_delivery_date", + "is_free_item", "section_break_5", "description", "item_group", @@ -19,20 +21,18 @@ "col_break1", "image", "image_view", - "manufacture_details", - "manufacturer", - "column_break_15", - "manufacturer_part_no", "quantity_and_rate", "qty", "stock_uom", - "price_list_rate", - "discount_percentage", - "discount_amount", "col_break2", "uom", "conversion_factor", "stock_qty", + "sec_break_price_list", + "price_list_rate", + "discount_percentage", + "discount_amount", + "col_break_price_list", "base_price_list_rate", "sec_break1", "rate", @@ -42,7 +42,6 @@ "base_rate", "base_amount", "pricing_rules", - "is_free_item", "section_break_24", "net_rate", "net_amount", @@ -56,7 +55,6 @@ "weight_uom", "warehouse_and_reference", "warehouse", - "project", "prevdoc_doctype", "material_request", "sales_order", @@ -65,13 +63,19 @@ "material_request_item", "request_for_quotation_item", "item_tax_rate", + "manufacture_details", + "manufacturer", + "column_break_15", + "manufacturer_part_no", + "ad_sec_break", + "project", "section_break_44", "page_break" ], "fields": [ { "bold": 1, - "columns": 4, + "columns": 2, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -107,7 +111,7 @@ { "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days" + "label": "Supplier Lead Time (days)" }, { "collapsible": 1, @@ -162,7 +166,6 @@ { "fieldname": "stock_uom", "fieldtype": "Link", - "in_list_view": 1, "label": "Stock UOM", "options": "UOM", "print_hide": 1, @@ -196,6 +199,7 @@ { "fieldname": "uom", "fieldtype": "Link", + "in_list_view": 1, "label": "UOM", "options": "UOM", "print_hide": 1, @@ -289,14 +293,6 @@ "print_hide": 1, "read_only": 1 }, - { - "default": "0", - "fieldname": "is_free_item", - "fieldtype": "Check", - "label": "Is Free Item", - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "section_break_24", "fieldtype": "Section Break" @@ -528,12 +524,43 @@ { "fieldname": "column_break_15", "fieldtype": "Column Break" + }, + { + "fieldname": "sec_break_price_list", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break_price_list", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "ad_sec_break", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "default": "0", + "depends_on": "is_free_item", + "fieldname": "is_free_item", + "fieldtype": "Check", + "label": "Is Free Item", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "bold": 1, + "fieldname": "expected_delivery_date", + "fieldtype": "Date", + "label": "Expected Delivery Date" } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-04-07 18:35:51.175947", + "modified": "2020-10-01 16:34:39.703033", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", From 5006345a0a55ff4bb01cf431af6a8b8b900e5428 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Oct 2020 20:41:05 +0530 Subject: [PATCH 08/37] chore: Fetch RFQs in Get Items from RFQ based on Supplier selected - Rfq will appear only if selected supplier is in the RFQ --- .../request_for_quotation.py | 28 +++++++++++++++++++ .../supplier_quotation/supplier_quotation.js | 6 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index fd04b0d2a6..76775343e4 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -354,3 +354,31 @@ def get_supplier_tag(): frappe.cache().hset("Supplier", "Tags", tags) return frappe.cache().hget("Supplier", "Tags") + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters): + conditions = "" + if txt: + conditions += "and rfq.name like '%%"+txt+"%%' " + + if filters.get("transaction_date"): + conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date")) + + rfq_data = frappe.db.sql(""" + select + distinct rfq.name, rfq.transaction_date, + rfq.company + from + `tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier + where + rfq.name = rfq_supplier.parent + and rfq_supplier.supplier = '{0}' + and rfq.docstatus = 1 + and rfq.company = '{1}' + {2} + order by rfq.transaction_date ASC + limit {3} offset {4} """ \ + .format(filters.get("supplier"), filters.get("company"), conditions, page_len, start), as_dict=1) + + return rfq_data \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index 4a2a0786f3..3376e82956 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -62,8 +62,10 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext transaction_date: null }, get_query_filters: { - docstatus: 1, - } + supplier: me.frm.doc.supplier + }, + get_query_method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_rfq_containing_supplier" + }) }, __("Get items from")); } From f57ba86bce5ad0b1ec647f90a8d8d0d42fd21a31 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Oct 2020 21:10:00 +0530 Subject: [PATCH 09/37] chore: Add Date and Valid Till to List View --- .../buying/doctype/supplier_quotation/supplier_quotation.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 660dcff34b..9a092ca5c3 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -159,6 +159,7 @@ "default": "Today", "fieldname": "transaction_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Date", "oldfieldname": "transaction_date", "oldfieldtype": "Date", @@ -798,6 +799,7 @@ { "fieldname": "valid_till", "fieldtype": "Date", + "in_list_view": 1, "label": "Valid Till" } ], @@ -805,7 +807,7 @@ "idx": 29, "is_submittable": 1, "links": [], - "modified": "2020-07-18 05:10:45.556792", + "modified": "2020-10-01 20:56:17.932007", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", From b9e2f17f4f427dc8726c348ddc2577c7d8b50f7b Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Oct 2020 21:31:51 +0530 Subject: [PATCH 10/37] fix: More secure query --- .../doctype/request_for_quotation/request_for_quotation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 76775343e4..361ccdf7f3 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -378,7 +378,8 @@ def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filt and rfq.company = '{1}' {2} order by rfq.transaction_date ASC - limit {3} offset {4} """ \ - .format(filters.get("supplier"), filters.get("company"), conditions, page_len, start), as_dict=1) + limit %(page_len)s offset %(start)s """ \ + .format(filters.get("supplier"), filters.get("company"), conditions), + {"page_len": page_len, "start": start}, as_dict=1) return rfq_data \ No newline at end of file From 0bc61b31f7f7d0eee17be43c26ee6c5048ed050b Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Oct 2020 11:54:54 +0530 Subject: [PATCH 11/37] chore: Move Qty fields (received qty,etc.) close to Qty/Stock UOM --- .../material_request_item.json | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index 08c273838f..25bbbbd4b3 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -26,6 +26,13 @@ "uom", "conversion_factor", "stock_qty", + "qty_info_sec_break", + "min_order_qty", + "projected_qty", + "qty_info_col_break", + "actual_qty", + "ordered_qty", + "received_qty", "rate_and_amount_section_break", "rate", "col_break3", @@ -45,13 +52,8 @@ "sales_order_item", "production_plan", "material_request_plan_item", - "expense_account", "col_break4", - "min_order_qty", - "projected_qty", - "actual_qty", - "ordered_qty", - "received_qty", + "expense_account", "section_break_46", "page_break" ], @@ -434,13 +436,21 @@ { "fieldname": "col_break_mfg", "fieldtype": "Column Break" + }, + { + "fieldname": "qty_info_sec_break", + "fieldtype": "Section Break" + }, + { + "fieldname": "qty_info_col_break", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-18 20:52:33.265074", + "modified": "2020-10-02 11:44:36.553064", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", From 203585f29250522bf559b96e75ebc34b07ce89ac Mon Sep 17 00:00:00 2001 From: michellealva Date: Tue, 6 Oct 2020 16:00:39 +0530 Subject: [PATCH 12/37] fix(Employee): Field level change in Exit section --- erpnext/hr/doctype/employee/employee.json | 33 ++++++----------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 8c02e4f1d6..da789198e5 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -109,7 +109,6 @@ "encashment_date", "exit_interview_details", "held_on", - "reason_for_resignation", "new_workplace", "feedback", "lft", @@ -682,7 +681,7 @@ }, { "fieldname": "reason_for_leaving", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Reason for Leaving", "oldfieldname": "reason_for_leaving", "oldfieldtype": "Data" @@ -696,6 +695,7 @@ "options": "\nYes\nNo" }, { + "depends_on": "eval:doc.leave_encashed ==\"Yes\"", "fieldname": "encashment_date", "fieldtype": "Date", "label": "Encashment Date", @@ -705,7 +705,6 @@ { "fieldname": "exit_interview_details", "fieldtype": "Column Break", - "label": "Exit Interview Details", "oldfieldname": "col_brk6", "oldfieldtype": "Column Break", "width": "50%" @@ -713,18 +712,10 @@ { "fieldname": "held_on", "fieldtype": "Date", - "label": "Held On", + "label": "Exit Interview Held On", "oldfieldname": "held_on", "oldfieldtype": "Date" }, - { - "fieldname": "reason_for_resignation", - "fieldtype": "Select", - "label": "Reason for Resignation", - "oldfieldname": "reason_for_resignation", - "oldfieldtype": "Select", - "options": "\nBetter Prospects\nHealth Concerns" - }, { "fieldname": "new_workplace", "fieldtype": "Data", @@ -809,37 +800,29 @@ "fieldname": "expense_approver", "fieldtype": "Link", "label": "Expense Approver", - "options": "User", - "show_days": 1, - "show_seconds": 1 + "options": "User" }, { "fieldname": "approvers_section", "fieldtype": "Section Break", - "label": "Approvers", - "show_days": 1, - "show_seconds": 1 + "label": "Approvers" }, { "fieldname": "column_break_45", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shift_request_approver", "fieldtype": "Link", "label": "Shift Request Approver", - "options": "User", - "show_days": 1, - "show_seconds": 1 + "options": "User" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2020-07-28 01:36:04.109189", + "modified": "2020-10-06 15:58:23.805489", "modified_by": "Administrator", "module": "HR", "name": "Employee", From 3525ddabc5007940ba6f5154144bb6afbe799c3e Mon Sep 17 00:00:00 2001 From: michellealva Date: Wed, 7 Oct 2020 19:02:19 +0530 Subject: [PATCH 13/37] fix: Add links in Stock desk page --- erpnext/stock/desk_page/stock/stock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 2fba5fa804..390fcd91e3 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Items and Pricing", - "links": "[\n {\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipping Rule\",\n \"name\": \"Shipping Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Alternative\",\n \"name\": \"Item Alternative\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Manufacturer\",\n \"name\": \"Item Manufacturer\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipping Rule\",\n \"name\": \"Shipping Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Alternative\",\n \"name\": \"Item Alternative\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Manufacturer\",\n \"name\": \"Item Manufacturer\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Customs Tariff Number\",\n \"name\": \"Customs Tariff Number\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Settings", - "links": "[\n {\n \"label\": \"Stock Settings\",\n \"name\": \"Stock Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Warehouse\",\n \"name\": \"Warehouse\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Unit of Measure (UOM)\",\n \"name\": \"UOM\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Variant Settings\",\n \"name\": \"Item Variant Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Brand\",\n \"name\": \"Brand\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Attribute\",\n \"name\": \"Item Attribute\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Stock Settings\",\n \"name\": \"Stock Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Warehouse\",\n \"name\": \"Warehouse\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Unit of Measure (UOM)\",\n \"name\": \"UOM\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Variant Settings\",\n \"name\": \"Item Variant Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Brand\",\n \"name\": \"Brand\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Attribute\",\n \"name\": \"Item Attribute\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"UOM Conversion Factor\",\n \"name\": \"UOM Conversion Factor\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-08-11 17:29:32.626067", + "modified": "2020-10-07 18:40:17.130207", "modified_by": "Administrator", "module": "Stock", "name": "Stock", From be0ebc50855e3f3404b6f8d5bbc78df6ed3a50ce Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira Date: Wed, 7 Oct 2020 20:47:12 +0530 Subject: [PATCH 14/37] spell fixes for Condition --- .../salary_component/salary_component.json | 9 ++++--- .../doctype/salary_detail/salary_detail.json | 24 ++++++------------- .../doctype/quotation/tests/test_quotation.js | 2 +- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/erpnext/payroll/doctype/salary_component/salary_component.json b/erpnext/payroll/doctype/salary_component/salary_component.json index 225b048293..c97e45cd53 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.json +++ b/erpnext/payroll/doctype/salary_component/salary_component.json @@ -217,7 +217,7 @@ "fieldname": "help", "fieldtype": "HTML", "label": "Help", - "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condition. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" }, { "default": "0", @@ -238,14 +238,13 @@ "depends_on": "eval:doc.type == \"Deduction\"", "fieldname": "is_income_tax_component", "fieldtype": "Check", - "label": "Is Income Tax Component", - "show_days": 1, - "show_seconds": 1 + "label": "Is Income Tax Component" } ], "icon": "fa fa-flag", + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-06-22 15:39:20.826565", + "modified": "2020-10-07 20:38:33.795853", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Component", diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index cc87caeae1..eedb56ec08 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -117,7 +117,7 @@ "depends_on": "eval:doc.is_flexible_benefit != 1", "fieldname": "section_break_2", "fieldtype": "Section Break", - "label": "Condtion and formula" + "label": "Condition and formula" }, { "allow_on_submit": 1, @@ -206,38 +206,28 @@ "collapsible": 1, "fieldname": "section_break_5", "fieldtype": "Section Break", - "label": "Component properties and references ", - "show_days": 1, - "show_seconds": 1 + "label": "Component properties and references " }, { "fieldname": "column_break_11", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "section_break_19", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "column_break_18", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "column_break_24", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2020-07-01 12:13:41.956495", + "modified": "2020-10-07 20:39:41.619283", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation.js b/erpnext/selling/doctype/quotation/tests/test_quotation.js index d69d799d0d..ad942fe497 100644 --- a/erpnext/selling/doctype/quotation/tests/test_quotation.js +++ b/erpnext/selling/doctype/quotation/tests/test_quotation.js @@ -46,7 +46,7 @@ QUnit.test("test: quotation", function (assert) { assert.ok(cur_frm.doc.items[0].rate == 200, "Price Changed Manually"); assert.equal(cur_frm.doc.total, 1000, "New Total Calculated"); - // Check Terms and Condtions + // Check Terms and Conditions assert.ok(cur_frm.doc.tc_name == "Test Term 1", "Terms and Conditions Checked"); assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct"); From 40101964883c60270e7e212460e7e8e67dd7a28f Mon Sep 17 00:00:00 2001 From: Tunde Akinyanmi Date: Thu, 8 Oct 2020 02:04:53 +0100 Subject: [PATCH 15/37] fix: pass self.flags.ignore_permissions to the ignore_permissions argument of Address and Contact controller --- erpnext/selling/doctype/customer/customer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 1f955fcd52..0172d9c128 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -132,7 +132,7 @@ class Customer(TransactionBase): address = frappe.get_doc('Address', address_name.get('name')) if not address.has_link('Customer', self.name): address.append('links', dict(link_doctype='Customer', link_name=self.name)) - address.save() + address.save(ignore_permissions=self.flags.ignore_permissions) lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True) @@ -150,7 +150,7 @@ class Customer(TransactionBase): contact = frappe.get_doc('Contact', contact_name.get('name')) if not contact.has_link('Customer', self.name): contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - contact.save() + contact.save(ignore_permissions=self.flags.ignore_permissions) else: lead.lead_name = lead.lead_name.lstrip().split(" ") From a86cab0bb2f3088237335e682603a87da50cc3ec Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 8 Oct 2020 16:41:23 +0530 Subject: [PATCH 16/37] fix: added patch to make custom field 'Print UOM after Quantity' --- erpnext/patches.txt | 1 + .../patches/v13_0/print_uom_after_quantity_patch.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 erpnext/patches/v13_0/print_uom_after_quantity_patch.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 6087ce29aa..77310cd70e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -729,3 +729,4 @@ erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports erpnext.patches.v13_0.rename_issue_doctype_fields erpnext.patches.v13_0.change_default_pos_print_format erpnext.patches.v13_0.set_youtube_video_id +erpnext.patches.v13_0.print_uom_after_quantity_patch \ No newline at end of file diff --git a/erpnext/patches/v13_0/print_uom_after_quantity_patch.py b/erpnext/patches/v13_0/print_uom_after_quantity_patch.py new file mode 100644 index 0000000000..0de3728f5c --- /dev/null +++ b/erpnext/patches/v13_0/print_uom_after_quantity_patch.py @@ -0,0 +1,10 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from erpnext.setup.install import create_print_uom_after_qty_custom_field + +def execute(): + create_print_uom_after_qty_custom_field() From 5e6ea369380e379d635aabe230125a7bd527e2ae Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 8 Oct 2020 20:32:36 +0530 Subject: [PATCH 17/37] fix: Add lower deduction certificate in desk page --- erpnext/accounts/desk_page/accounting/accounting.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index 3f23ba9019..045d8fb8a9 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -53,7 +53,7 @@ { "hidden": 0, "label": "Goods and Services Tax (GST India)", - "links": "[\n {\n \"label\": \"GST Settings\",\n \"name\": \"GST Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"GST HSN Code\",\n \"name\": \"GST HSN Code\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-1\",\n \"name\": \"GSTR-1\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-2\",\n \"name\": \"GSTR-2\",\n \"type\": \"report\"\n },\n {\n \"label\": \"GSTR 3B Report\",\n \"name\": \"GSTR 3B Report\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Sales Register\",\n \"name\": \"GST Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Purchase Register\",\n \"name\": \"GST Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Sales Register\",\n \"name\": \"GST Itemised Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Purchase Register\",\n \"name\": \"GST Itemised Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"country\": \"India\",\n \"description\": \"C-Form records\",\n \"label\": \"C-Form\",\n \"name\": \"C-Form\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"GST Settings\",\n \"name\": \"GST Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"GST HSN Code\",\n \"name\": \"GST HSN Code\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-1\",\n \"name\": \"GSTR-1\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-2\",\n \"name\": \"GSTR-2\",\n \"type\": \"report\"\n },\n {\n \"label\": \"GSTR 3B Report\",\n \"name\": \"GSTR 3B Report\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Sales Register\",\n \"name\": \"GST Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Purchase Register\",\n \"name\": \"GST Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Sales Register\",\n \"name\": \"GST Itemised Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Purchase Register\",\n \"name\": \"GST Itemised Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"country\": \"India\",\n \"description\": \"C-Form records\",\n \"label\": \"C-Form\",\n \"name\": \"C-Form\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"India\",\n \"label\": \"Lower Deduction Certificate\",\n \"name\": \"Lower Deduction Certificate\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-09-09 11:45:33.766400", + "modified": "2020-10-08 20:31:46.022470", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", From 22dc42efb98192a80372374cfc37cad2c6c79d08 Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 7 Oct 2020 18:16:18 +0530 Subject: [PATCH 18/37] fix: error log while creating company --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8e707fe3f4..4a438f70e6 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -442,7 +442,7 @@ def install_country_fixtures(company): module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(company_doc.country)) frappe.get_attr(module_name)(company_doc, False) except Exception as e: - frappe.log_error(str(e), frappe.get_traceback()) + frappe.log_error() frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country))) From 776d8f72cf0b0ff2a5e19d8cafe6cffcadac57b5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 9 Oct 2020 15:36:11 +0530 Subject: [PATCH 19/37] chore: do not allow future transactions --- erpnext/utilities/transaction_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index c8e3330908..7f27ab6511 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import frappe.share from frappe import _ -from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_datetime, get_link_to_form +from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_datetime, get_link_to_form, date_diff, nowdate from erpnext.controllers.status_updater import StatusUpdater from erpnext.accounts.utils import get_fiscal_year @@ -29,7 +29,13 @@ class TransactionBase(StatusUpdater): except ValueError: frappe.throw(_('Invalid Posting Time')) + self.validate_future_posting() self.validate_with_last_transaction_posting_time() + + def validate_future_posting(self): + if getattr(self, 'set_posting_time', None) and date_diff(self.posting_date, nowdate()) > 0: + msg = _("Posting future transactions are not allowed due to Immutable Ledger") + frappe.throw(msg, title=_("Future Posting Not Allowed")) def add_calendar_event(self, opts, force=False): if cstr(self.contact_by) != cstr(self._prev.contact_by) or \ From 1bc7fc41a5fdcd2746ab85ea83e5a7ea1584328b Mon Sep 17 00:00:00 2001 From: michellealva Date: Fri, 9 Oct 2020 16:01:13 +0530 Subject: [PATCH 20/37] fix: Change name of Sales Team table --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 2397b7d0cb..cd66f7a086 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1825,7 +1825,7 @@ "fieldtype": "Table", "hide_days": 1, "hide_seconds": 1, - "label": "Sales Team1", + "label": "Sales Contributions and Incentives", "oldfieldname": "sales_team", "oldfieldtype": "Table", "options": "Sales Team", @@ -1946,7 +1946,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-08-27 01:56:28.532140", + "modified": "2020-10-09 15:59:57.544736", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From bd6d5eb5ac40cfe8463f45abf7235321bb668675 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 9 Oct 2020 20:57:17 +0530 Subject: [PATCH 21/37] fix: Remove Production Order reference from Item Validation --- erpnext/stock/doctype/item/item.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d22fda85f4..bfdddbbb60 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -984,9 +984,7 @@ class Item(WebsiteGenerator): if self.stock_ledger_created(): return True - elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}) or \ - frappe.db.get_value("Production Order", - filters={"production_item": self.name, "docstatus": 1}): + elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}): return True def validate_auto_reorder_enabled_in_stock_settings(self): From d927c8efbaf98ca33d8ad0a94329bd1a6f5ea549 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 9 Oct 2020 21:00:33 +0530 Subject: [PATCH 22/37] fix: (revert) Add Delivery Note Count in Sales Invoice Dashboard --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 2980213f3b..f1069282ed 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,8 +13,7 @@ def get_data(): 'Auto Repeat': 'reference_document', }, 'internal_links': { - 'Sales Order': ['items', 'sales_order'], - 'Delivery Note': ['items', 'delivery_note'] + 'Sales Order': ['items', 'sales_order'] }, 'transactions': [ { From 56fea7d2438dde58f58d6f72766e1fc9e0d00d02 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Oct 2020 21:19:25 +0530 Subject: [PATCH 23/37] fix: update items after submission ignores precision (#23491) * fix: update items after submission ignores precision * chore: add test --- erpnext/controllers/accounts_controller.py | 12 +++++++----- erpnext/public/js/utils.js | 12 +++++++++--- .../doctype/sales_order/test_sales_order.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bb288c5551..166564961d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1319,24 +1319,26 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil validate_quantity(child_item, d) child_item.qty = flt(d.get("qty")) - precision = child_item.precision("rate") or 2 + rate_precision = child_item.precision("rate") or 2 + conv_fac_precision = child_item.precision("conversion_factor") or 2 + qty_precision = child_item.precision("qty") or 2 - if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision): + if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision): frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.") .format(child_item.idx, child_item.item_code)) else: - child_item.rate = flt(d.get("rate")) + child_item.rate = flt(d.get("rate"), rate_precision) if d.get("conversion_factor"): if child_item.stock_uom == child_item.uom: child_item.conversion_factor = 1 else: - child_item.conversion_factor = flt(d.get('conversion_factor')) + child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision) if d.get("uom"): child_item.uom = d.get("uom") conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")) - child_item.conversion_factor = flt(d.get('conversion_factor')) or conversion_factor + child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision) or conversion_factor if d.get("delivery_date") and parent_doctype == 'Sales Order': child_item.delivery_date = d.get('delivery_date') diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 9ed500932f..6d73418821 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -452,6 +452,9 @@ erpnext.utils.update_child_items = function(opts) { const frm = opts.frm; const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; + const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); + const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; + this.data = []; const fields = [{ fieldtype:'Data', @@ -499,14 +502,16 @@ erpnext.utils.update_child_items = function(opts) { default: 0, read_only: 0, in_list_view: 1, - label: __('Qty') + label: __('Qty'), + precision: get_precision("qty") }, { fieldtype:'Currency', fieldname:"rate", default: 0, read_only: 0, in_list_view: 1, - label: __('Rate') + label: __('Rate'), + precision: get_precision("rate") }]; if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { @@ -521,7 +526,8 @@ erpnext.utils.update_child_items = function(opts) { fieldtype: 'Float', fieldname: "conversion_factor", in_list_view: 1, - label: __("Conversion Factor") + label: __("Conversion Factor"), + precision: get_precision('conversion_factor') }) } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 5aef096d4b..1ce36dd8bf 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -403,6 +403,22 @@ class TestSalesOrder(unittest.TestCase): trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + + def test_update_child_with_precision(self): + from frappe.model.meta import get_field_precision + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + + precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate")) + + make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency") + so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664) + + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}]) + update_child_qty_rate('Sales Order', trans_item, so.name) + + so.reload() + self.assertEqual(so.items[0].rate, 200.34669) + make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency") def test_update_child_perm(self): so = make_sales_order(item_code= "_Test Item", qty=4) From 07731c77354351a3b250fa51f41156fee4e7b13c Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 9 Oct 2020 21:44:23 +0530 Subject: [PATCH 24/37] fix: Check if list view standard filter exists in Payment Entry --- .../doctype/payment_entry/payment_entry_list.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js index 7ea60bb48e..e6d83b9f68 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry_list.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry_list.js @@ -1,12 +1,14 @@ frappe.listview_settings['Payment Entry'] = { onload: function(listview) { - listview.page.fields_dict.party_type.get_query = function() { - return { - "filters": { - "name": ["in", Object.keys(frappe.boot.party_account_types)], - } + if (listview.page.fields_dict.party_type) { + listview.page.fields_dict.party_type.get_query = function() { + return { + "filters": { + "name": ["in", Object.keys(frappe.boot.party_account_types)], + } + }; }; - }; + } } }; \ No newline at end of file From 2676f962031a3d3223349e13e7355ebe616b569a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Oct 2020 21:58:38 +0530 Subject: [PATCH 25/37] fix: Opening Journal Entry via Data Import --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 34c262e27f..d8394785c6 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -22,8 +22,12 @@ class JournalEntry(AccountsController): return self.voucher_type def validate(self): + if self.voucher_type == 'Opening Entry': + self.is_opening = 'Yes' + if not self.is_opening: self.is_opening='No' + self.clearance_date = None self.validate_party() From 654c36ea99fa2de9e537c4000cfcb3b55a105d59 Mon Sep 17 00:00:00 2001 From: Kanchan Chauhan Date: Thu, 8 Oct 2020 16:37:05 +0530 Subject: [PATCH 26/37] feat: Supplier Sourced Items in BOM --- erpnext/controllers/buying_controller.py | 1 + erpnext/manufacturing/doctype/bom/bom.js | 19 +- erpnext/manufacturing/doctype/bom/bom.py | 25 +- erpnext/manufacturing/doctype/bom/test_bom.py | 70 ++ .../bom_explosion_item.json | 761 ++++-------------- .../doctype/bom_item/bom_item.json | 17 +- 6 files changed, 277 insertions(+), 616 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index ac567b7dea..e05c70e41c 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -847,6 +847,7 @@ def get_items_from_bom(item_code, bom, exploded_item=1): where t2.parent = t1.name and t1.item = %s and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s + and t2.sourced_by_supplier = 0 and t2.item_code = t3.name""".format(doctype), (item_code, bom), as_dict=1) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 47b4207241..1c4b7a1e1c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -434,7 +434,8 @@ var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { "include_item_in_manufacturing": d.include_item_in_manufacturing, "uom": d.uom, "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor + "conversion_factor": d.conversion_factor, + "sourced_by_supplier": d.sourced_by_supplier }, callback: function(r) { d = locals[cdt][cdn]; @@ -616,6 +617,22 @@ frappe.ui.form.on("BOM Item", "item_code", function(frm, cdt, cdn) { refresh_field("allow_alternative_item", d.name, d.parentfield); }); +frappe.ui.form.on("BOM Item", "sourced_by_supplier", function(frm, cdt, cdn) { + var d = locals[cdt][cdn]; + if (d.sourced_by_supplier) { + d.rate = 0; + refresh_field("rate", d.name, d.parentfield); + } +}); + +frappe.ui.form.on("BOM Item", "rate", function(frm, cdt, cdn) { + var d = locals[cdt][cdn]; + if (d.sourced_by_supplier) { + d.rate = 0; + refresh_field("rate", d.name, d.parentfield); + } +}); + frappe.ui.form.on("BOM Operation", "operations_remove", function(frm) { erpnext.bom.calculate_op_cost(frm.doc); erpnext.bom.calculate_total(frm.doc); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3189433837..71d49a9537 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -137,7 +137,8 @@ class BOM(WebsiteGenerator): "qty": item.qty, "uom": item.uom, "stock_uom": item.stock_uom, - "conversion_factor": item.conversion_factor + "conversion_factor": item.conversion_factor, + "sourced_by_supplier": item.sourced_by_supplier }) for r in ret: if not item.get(r): @@ -172,7 +173,8 @@ class BOM(WebsiteGenerator): 'qty' : args.get("qty") or args.get("stock_qty") or 1, 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), - 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0 + 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0, + 'sourced_by_supplier' : args['sourced_by_supplier'] or 0 } return ret_item @@ -191,8 +193,8 @@ class BOM(WebsiteGenerator): if arg.get('scrap_items'): rate = get_valuation_rate(arg) elif arg: - #Customer Provided parts will have zero rate - if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item'): + #Customer Provided parts and Supplier sourced parts will have zero rate + if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item') and not arg.get('sourced_by_supplier'): if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom: rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1) else: @@ -205,7 +207,6 @@ class BOM(WebsiteGenerator): else: frappe.msgprint(_("{0} not found for item {1}") .format(self.rm_cost_as_per, arg["item_code"]), alert=True) - return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) def update_cost(self, update_parent=True, from_child_bom=False, save=True): @@ -221,7 +222,8 @@ class BOM(WebsiteGenerator): "qty": d.qty, "uom": d.uom, "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor + "conversion_factor": d.conversion_factor, + "sourced_by_supplier": d.sourced_by_supplier }) if rate: @@ -495,7 +497,8 @@ class BOM(WebsiteGenerator): 'stock_uom' : d.stock_uom, 'stock_qty' : flt(d.stock_qty), 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), - 'include_item_in_manufacturing': d.include_item_in_manufacturing + 'include_item_in_manufacturing': d.include_item_in_manufacturing, + 'sourced_by_supplier': d.sourced_by_supplier })) def company_currency(self): @@ -521,6 +524,7 @@ class BOM(WebsiteGenerator): bom_item.stock_qty, bom_item.rate, bom_item.include_item_in_manufacturing, + bom_item.sourced_by_supplier, bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit FROM `tabBOM Explosion Item` bom_item, tabBOM bom WHERE @@ -539,7 +543,8 @@ class BOM(WebsiteGenerator): 'stock_uom' : d['stock_uom'], 'stock_qty' : d['qty_consumed_per_unit'] * stock_qty, 'rate' : flt(d['rate']), - 'include_item_in_manufacturing': d.get('include_item_in_manufacturing', 0) + 'include_item_in_manufacturing': d.get('include_item_in_manufacturing', 0), + 'sourced_by_supplier': d.get('sourced_by_supplier', 0) })) def add_exploded_items(self): @@ -679,7 +684,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite is_stock_item=is_stock_item, qty_field="stock_qty", select_columns = """, bom_item.source_warehouse, bom_item.operation, - bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, + bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier, (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""") items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) @@ -692,7 +697,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item, qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", select_columns = """, bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, - bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, + bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, bom_item.description, bom_item.base_rate as rate """) items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 3dfd03b139..eface4d624 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -10,6 +10,8 @@ from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from six import string_types +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order test_records = frappe.get_test_records('BOM') @@ -138,6 +140,74 @@ class TestBOM(unittest.TestCase): self.assertEqual(bom.items[0].rate, 20) + def test_subcontractor_sourced_item(self): + item_code = "_Test Subcontracted FG Item 1" + + if not frappe.db.exists('Item', item_code): + make_item(item_code, { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'stock_uom': 'Nos' + }) + + if not frappe.db.exists('Item', "Test Extra Item 1"): + make_item("Test Extra Item 1", { + 'is_stock_item': 1, + 'stock_uom': 'Nos' + }) + + if not frappe.db.exists('Item', "Test Extra Item 2"): + make_item("Test Extra Item 2", { + 'is_stock_item': 1, + 'stock_uom': 'Nos' + }) + + if not frappe.db.exists('Item', "Test Extra Item 3"): + make_item("Test Extra Item 3", { + 'is_stock_item': 1, + 'stock_uom': 'Nos' + }) + bom = frappe.get_doc({ + 'doctype': 'BOM', + 'is_default': 1, + 'item': item_code, + 'currency': 'USD', + 'quantity': 1, + 'company': '_Test Company' + }) + + for item in ["Test Extra Item 1", "Test Extra Item 2"]: + item_doc = frappe.get_doc('Item', item) + + bom.append('items', { + 'item_code': item, + 'qty': 1, + 'uom': item_doc.stock_uom, + 'stock_uom': item_doc.stock_uom, + 'rate': item_doc.valuation_rate + }) + + bom.append('items', { + 'item_code': "Test Extra Item 3", + 'qty': 1, + 'uom': item_doc.stock_uom, + 'stock_uom': item_doc.stock_uom, + 'rate': 0, + 'sourced_by_supplier': 1 + }) + bom.insert(ignore_permissions=True) + bom.update_cost() + bom.submit() + # test that sourced_by_supplier rate is zero even after updating cost + self.assertEqual(bom.items[2].rate, 0) + # test in Purchase Order sourced_by_supplier is not added to Supplied Item + po = create_purchase_order(item_code=item_code, qty=1, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1]) + supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) + self.assertEquals(bom_items, supplied_items) + + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json index 9fadbef0f5..f01d856e72 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json @@ -1,626 +1,181 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-03-07 11:42:57", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2013-03-07 11:42:57", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "cb", + "source_warehouse", + "operation", + "section_break_3", + "description", + "column_break_2", + "image", + "image_view", + "section_break_4", + "stock_qty", + "rate", + "qty_consumed_per_unit", + "column_break_8", + "stock_uom", + "amount", + "include_item_in_manufacturing", + "sourced_by_supplier" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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": 1, - "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": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "cb", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "source_warehouse", - "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": "Source Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "source_warehouse", + "fieldtype": "Link", + "label": "Source Warehouse", + "options": "Warehouse", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operation", - "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": "Operation", - "length": 0, - "no_copy": 0, - "options": "Operation", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "operation", + "fieldtype": "Link", + "label": "Operation", + "options": "Operation", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_3", - "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, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "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": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "read_only": 1, "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 1, + "label": "Image", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image_view", - "fieldtype": "Image", - "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": "Image View", - "length": 0, - "no_copy": 0, - "options": "image", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "image_view", + "fieldtype": "Image", + "label": "Image View", + "options": "image" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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": "Stock Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "qty", - "oldfieldtype": "Currency", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Stock Qty", + "oldfieldname": "qty", + "oldfieldtype": "Currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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, - "oldfieldname": "standard_rate", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "oldfieldname": "standard_rate", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty_consumed_per_unit", - "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 Consumed Per Unit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "qty_consumed_per_unit", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty Consumed Per Unit", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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, - "oldfieldname": "stock_uom", - "oldfieldtype": "Link", - "options": "UOM", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "oldfieldname": "stock_uom", + "oldfieldtype": "Link", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 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": 1, - "in_standard_filter": 0, - "label": "Amount", - "length": 0, - "no_copy": 0, - "oldfieldname": "amount_as_per_sr", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "oldfieldname": "amount_as_per_sr", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "include_item_in_manufacturing", - "fieldtype": "Check", - "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": "Include Item In Manufacturing", - "length": 0, - "no_copy": 0, - "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, - "translatable": 0, - "unique": 0 + "default": "0", + "fieldname": "include_item_in_manufacturing", + "fieldtype": "Check", + "label": "Include Item In Manufacturing", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "sourced_by_supplier", + "fieldtype": "Check", + "label": "Sourced by Supplier", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-20 19:04:59.813773", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Explosion Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-10-08 16:21:29.386212", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Explosion Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index e34be61bc7..4c9877f52b 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -37,7 +37,9 @@ "section_break_27", "has_variants", "include_item_in_manufacturing", - "original_item" + "original_item", + "column_break_33", + "sourced_by_supplier" ], "fields": [ { @@ -272,12 +274,23 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "sourced_by_supplier", + "fieldtype": "Check", + "label": "Sourced by Supplier" } ], "idx": 1, + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-04-09 14:30:26.535546", + "modified": "2020-10-08 14:19:37.563300", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", From 3d91d48b7786e5bc37fa82bd24f4e76512ef2e4b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 10 Oct 2020 21:56:50 +0530 Subject: [PATCH 27/37] fix: validate only for stock transactions --- erpnext/utilities/transaction_base.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 7f27ab6511..298e1110d3 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -32,7 +32,21 @@ class TransactionBase(StatusUpdater): self.validate_future_posting() self.validate_with_last_transaction_posting_time() + def is_stock_transaction(self): + if self.doctype not in ["Sales Invoice", "Purchase Invoice", "Stock Entry", "Stock Reconciliation", + "Delivery Note", "Purchase Receipt", "Fees"]: + return False + + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + if not (self.get("update_stock") or self.get("is_pos")): + return False + + return True + def validate_future_posting(self): + if not self.is_stock_transaction(): + return + if getattr(self, 'set_posting_time', None) and date_diff(self.posting_date, nowdate()) > 0: msg = _("Posting future transactions are not allowed due to Immutable Ledger") frappe.throw(msg, title=_("Future Posting Not Allowed")) @@ -168,13 +182,8 @@ class TransactionBase(StatusUpdater): def validate_with_last_transaction_posting_time(self): - if self.doctype not in ["Sales Invoice", "Purchase Invoice", "Stock Entry", "Stock Reconciliation", - "Delivery Note", "Purchase Receipt", "Fees"]: - return - - if self.doctype in ["Sales Invoice", "Purchase Invoice"]: - if not (self.get("update_stock") or self.get("is_pos")): - return + if not self.is_stock_transaction(): + return for item in self.get('items'): last_transaction_time = frappe.db.sql(""" From 2be23309bb703edfb1c356b900ecef943ac7f247 Mon Sep 17 00:00:00 2001 From: Anupam Date: Sun, 11 Oct 2020 14:14:50 +0530 Subject: [PATCH 28/37] fix: can't add child table in item price --- erpnext/stock/doctype/item_price/item_price.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 8e39eb5037..51b47c50a3 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -50,16 +50,18 @@ class ItemPrice(Document): def check_duplicates(self): conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s" + condition_data_dict = dict(item_code=self.item_code, price_list=self.price_list, name=self.name) for field in ['uom', 'valid_from', 'valid_upto', 'packing_unit', 'customer', 'supplier']: if self.get(field): conditions += " and {0} = %({1})s".format(field, field) + condition_data_dict[field] = self.get(field) price_list_rate = frappe.db.sql(""" SELECT price_list_rate FROM `tabItem Price` - {conditions} """.format(conditions=conditions), self.as_dict()) + {conditions} """.format(conditions=conditions), condition_data_dict) if price_list_rate : frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem) From 7a86a77322122efa0565f00d21fb6a8f46a1158e Mon Sep 17 00:00:00 2001 From: Kevin Chan Date: Mon, 12 Oct 2020 11:30:36 +0800 Subject: [PATCH 29/37] fix: Asset status after maintenance or repair Fix a bug where an **Asset**'s status is not being set to what it was previously after an **Asset Repair** or **Asset Maintenance**. --- erpnext/assets/doctype/asset/asset.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index efdbdb1fbf..45fdc9316e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -131,7 +131,7 @@ class Asset(AccountsController): def validate_gross_and_purchase_amount(self): if self.is_existing_asset: return - + if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount: frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\ Please do not book expense of multiple assets against one single Asset.") @@ -561,14 +561,18 @@ class Asset(AccountsController): return 100 * (1 - flt(depreciation_rate, float_precision)) def update_maintenance_status(): - assets = frappe.get_all('Asset', filters = {'docstatus': 1, 'maintenance_required': 1}) + assets = frappe.get_all( + "Asset", filters={"docstatus": 1, "maintenance_required": 1} + ) for asset in assets: asset = frappe.get_doc("Asset", asset.name) - if frappe.db.exists('Asset Maintenance Task', {'parent': asset.name, 'next_due_date': today()}): - asset.set_status('In Maintenance') - if frappe.db.exists('Asset Repair', {'asset_name': asset.name, 'repair_status': 'Pending'}): - asset.set_status('Out of Order') + if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}): + asset.set_status("Out of Order") + elif frappe.db.exists("Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}): + asset.set_status("In Maintenance") + else: + asset.set_status() def make_post_gl_entry(): From 03975a692e579656195ff16e2f31c1395bef60fa Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 12 Oct 2020 12:03:07 +0530 Subject: [PATCH 30/37] fix: Perform Item Attribute Value Validation on Variants Only --- .../doctype/item_attribute/item_attribute.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 7f00201587..3764738e83 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -29,9 +29,18 @@ class ItemAttribute(Document): '''Validate that if there are existing items with attributes, they are valid''' attributes_list = [d.attribute_value for d in self.item_attribute_values] - for item in frappe.db.sql('''select i.name, iva.attribute_value as value - from `tabItem Variant Attribute` iva, `tabItem` i where iva.attribute = %s - and iva.parent = i.name and i.has_variants = 0''', self.name, as_dict=1): + # Get Item Variant Attribute details of variant items + items = frappe.db.sql(""" + select + i.name, iva.attribute_value as value + from + `tabItem Variant Attribute` iva, `tabItem` i + where + iva.attribute = %(attribute)s + and iva.parent = i.name and + i.variant_of is not null and i.variant_of != ''""", {"attribute" : self.name}, as_dict=1) + + for item in items: if self.numeric_values: validate_is_incremental(self, self.name, item.value, item.name) else: From efc3e1fa6212911563445b8bf1e1fd756fd974f3 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 1 Oct 2020 15:55:04 +0530 Subject: [PATCH 31/37] fix: Payment Schedule not fetching --- .../accounts/doctype/subscription/subscription.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 07525317aa..552a5d476b 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -345,13 +345,14 @@ class Subscription(Document): invoice.set_taxes() # Due date - invoice.append( - 'payment_schedule', - { - 'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), - 'invoice_portion': 100 - } - ) + if self.days_until_due: + invoice.append( + 'payment_schedule', + { + 'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), + 'invoice_portion': 100 + } + ) # Discounts if self.additional_discount_percentage: From 15d46040ff1546e205f3e1044aefa15642fa225a Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 12 Oct 2020 12:39:08 +0530 Subject: [PATCH 32/37] fix: Do not consider opening entries for TDS calculation (#23597) --- .../tax_withholding_category/tax_withholding_category.py | 2 ++ 1 file changed, 2 insertions(+) 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 83d7967f79..8b5e68b359 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai from `tabGL Entry` where company = %s and party in %s and fiscal_year=%s and credit > 0 + and is_opening = 'No' """, (company, tuple(suppliers), fiscal_year), as_dict=1) vouchers = [d.voucher_no for d in entries] @@ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No select distinct voucher_no from `tabGL Entry` where party in %s and %s and debit > 0 + and is_opening = 'No' """, (tuple(suppliers), condition)) or [] def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): From 6ec3dc545392e013bb5fce737837d46ed8a934a6 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 12 Oct 2020 02:26:55 -0700 Subject: [PATCH 33/37] fix(asset): cannot create asset if cwip disabled and account not set (#23580) * Never add an asset GL entry if CWIP is not enabled * fix: asset purchase with purchase invoice * chore: allow enable cwip accounting only if cwip account is set * fix: cannot create asset if cwip disabled and account not set Co-authored-by: Saqib Ansari --- .../purchase_invoice/purchase_invoice.py | 3 +- .../purchase_invoice/test_purchase_invoice.py | 3 +- erpnext/assets/doctype/asset/asset.py | 75 +++++---- erpnext/assets/doctype/asset/test_asset.py | 144 +++++++++--------- .../doctype/asset_category/asset_category.py | 18 ++- .../asset_category/test_asset_category.py | 20 ++- 6 files changed, 153 insertions(+), 110 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 079f599706..c5260a1239 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -711,7 +711,8 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount / self.conversion_rate) }, item=item)) else: - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + cwip_account = get_asset_account("capital_work_in_progress_account", + asset_category=item.asset_category,company=self.company) cwip_account_currency = get_account_currency(cwip_account) gl_entries.append(self.get_gl_dict({ diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 9a666bf9f8..2e5a7142a3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1002,7 +1002,8 @@ def make_purchase_invoice(**args): "cost_center": args.cost_center or "_Test Cost Center - _TC", "project": args.project, "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "" + "rejected_serial_no": args.rejected_serial_no or "", + "asset_location": args.location or "" }) if args.get_taxes_and_charges: diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index efdbdb1fbf..fca9559504 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -466,50 +466,63 @@ class Asset(AccountsController): def validate_make_gl_entry(self): purchase_document = self.get_purchase_document() - asset_bought_with_invoice = purchase_document == self.purchase_invoice - fixed_asset_account, cwip_account = self.get_asset_accounts() - cwip_enabled = is_cwip_accounting_enabled(self.asset_category) - # check if expense already has been booked in case of cwip was enabled after purchasing asset - expense_booked = False - cwip_booked = False - - if asset_bought_with_invoice: - expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", - (purchase_document, fixed_asset_account), as_dict=1) - else: - cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", - (purchase_document, cwip_account), as_dict=1) - - if cwip_enabled and (expense_booked or not cwip_booked): - # if expense has already booked from invoice or cwip is booked from receipt + if not purchase_document: return False - elif not cwip_enabled and (not expense_booked or cwip_booked): - # if cwip is disabled but expense hasn't been booked yet - return True - elif cwip_enabled: - # default condition - return True + + asset_bought_with_invoice = (purchase_document == self.purchase_invoice) + fixed_asset_account = self.get_fixed_asset_account() + + cwip_enabled = is_cwip_accounting_enabled(self.asset_category) + cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled) + + query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""" + if asset_bought_with_invoice: + # with invoice purchase either expense or cwip has been booked + expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1) + if expense_booked: + # if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled + return False + + cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1) + if cwip_booked: + # if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled + return True + else: + # with receipt purchase either cwip has been booked or no entries have been made + if not cwip_account: + # if cwip account isn't available do not make gl entries + return False + + cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1) + # if cwip is not booked from receipt then do not make gl entries + # if cwip is booked from receipt then make gl entries + return cwip_booked def get_purchase_document(self): asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt return purchase_document + + def get_fixed_asset_account(self): + return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + + def get_cwip_account(self, cwip_enabled=False): + cwip_account = None + try: + cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company) + except: + # if no cwip account found in category or company and "cwip is enabled" then raise else silently pass + if cwip_enabled: + raise - def get_asset_accounts(self): - fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, - asset_category = self.asset_category, company = self.company) - - cwip_account = get_asset_account("capital_work_in_progress_account", - self.name, self.asset_category, self.company) - - return fixed_asset_account, cwip_account + return cwip_account def make_gl_entries(self): gl_entries = [] purchase_document = self.get_purchase_document() - fixed_asset_account, cwip_account = self.get_asset_accounts() + fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 52039c183b..a0d76031fc 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset from erpnext.assets.doctype.asset.asset import make_sales_invoice from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice class TestAsset(unittest.TestCase): @@ -558,81 +559,6 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) - def test_gle_with_cwip_toggling(self): - # TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset - frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) - - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=5000, do_not_submit=True, location="Test Location") - pr.set('taxes', [{ - 'category': 'Total', - 'add_deduct_tax': 'Add', - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Service Tax - _TC', - 'description': '_Test Account Service Tax', - 'cost_center': 'Main - _TC', - 'rate': 5.0 - }, { - 'category': 'Valuation and Total', - 'add_deduct_tax': 'Add', - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Shipping Charges - _TC', - 'description': '_Test Account Shipping Charges', - 'cost_center': 'Main - _TC', - 'rate': 5.0 - }]) - pr.submit() - expected_gle = ( - ("Asset Received But Not Billed - _TC", 0.0, 5250.0), - ("CWIP Account - _TC", 5250.0, 0.0) - ) - pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Receipt' and voucher_no = %s - order by account""", pr.name) - self.assertEqual(pr_gle, expected_gle) - - pi = make_invoice(pr.name) - pi.submit() - expected_gle = ( - ("_Test Account Service Tax - _TC", 250.0, 0.0), - ("_Test Account Shipping Charges - _TC", 250.0, 0.0), - ("Asset Received But Not Billed - _TC", 5250.0, 0.0), - ("Creditors - _TC", 0.0, 5500.0), - ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), - ) - pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", pi.name) - self.assertEqual(pi_gle, expected_gle) - - asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') - asset_doc = frappe.get_doc('Asset', asset) - month_end_date = get_last_day(nowdate()) - asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) - asset_doc.append("finance_books", { - "expected_value_after_useful_life": 200, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date - }) - - # disable cwip and try submitting - frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) - asset_doc.submit() - # asset should have gl entries even if cwip is disabled - expected_gle = ( - ("_Test Fixed Asset - _TC", 5250.0, 0.0), - ("CWIP Account - _TC", 0.0, 5250.0) - ) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type='Asset' and voucher_no = %s - order by account""", asset_doc.name) - self.assertEqual(gle, expected_gle) - - frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) - def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") @@ -640,6 +566,74 @@ class TestAsset(unittest.TestCase): doc = make_invoice(pr.name) self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) + + def test_asset_cwip_toggling_cases(self): + cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting") + name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"]) + cwip_acc = "CWIP Account - _TC" + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "") + frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "") + + # case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set + pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertFalse(gle) + + # case 1 -- PR with cwip disabled, Asset with cwip enabled + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) + asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertFalse(gle) + + # case 2 -- PR with cwip enabled, Asset with cwip disabled + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertTrue(gle) + + # case 3 -- PI with cwip disabled, Asset with cwip enabled + pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertFalse(gle) + + # case 4 -- PI with cwip enabled, Asset with cwip disabled + pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertTrue(gle) + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) + frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc) def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 9a33fc14ac..46620d56e9 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from frappe.model.document import Document class AssetCategory(Document): @@ -13,6 +13,7 @@ class AssetCategory(Document): self.validate_finance_books() self.validate_account_types() self.validate_account_currency() + self.valide_cwip_account() def validate_finance_books(self): for d in self.finance_books: @@ -58,6 +59,21 @@ class AssetCategory(Document): frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), title=_("Invalid Account")) + + def valide_cwip_account(self): + if self.enable_cwip_accounting: + missing_cwip_accounts_for_company = [] + for d in self.accounts: + if (not d.capital_work_in_progress_account and + not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")): + missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name)) + + if missing_cwip_accounts_for_company: + msg = _("""To enable Capital Work in Progress Accounting, """) + msg += _("""you must select Capital Work in Progress Account in accounts table""") + msg += "

" + msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company)) + frappe.throw(msg, title=_("Missing Account")) @frappe.whitelist() diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py index b32f9b5020..39b79d6c50 100644 --- a/erpnext/assets/doctype/asset_category/test_asset_category.py +++ b/erpnext/assets/doctype/asset_category/test_asset_category.py @@ -26,4 +26,22 @@ class TestAssetCategory(unittest.TestCase): asset_category.insert() except frappe.DuplicateEntryError: pass - \ No newline at end of file + + def test_cwip_accounting(self): + company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account") + frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "") + + asset_category = frappe.new_doc("Asset Category") + asset_category.asset_category_name = "Computers" + asset_category.enable_cwip_accounting = 1 + + asset_category.total_number_of_depreciations = 3 + asset_category.frequency_of_depreciation = 3 + asset_category.append("accounts", { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC" + }) + + self.assertRaises(frappe.ValidationError, asset_category.insert) \ No newline at end of file From ebbe2858729fc393797b4b378e1429f3d176bb51 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Oct 2020 17:02:31 +0530 Subject: [PATCH 34/37] feat: (report) POS Register (#23313) * feat: pos register report * feat: group by fields in pos register * chore: add paid amount column * fix: (minor) remove redundant group by label --- .../accounts/report/pos_register/__init__.py | 0 .../report/pos_register/pos_register.js | 76 ++++++ .../report/pos_register/pos_register.json | 30 +++ .../report/pos_register/pos_register.py | 222 ++++++++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 erpnext/accounts/report/pos_register/__init__.py create mode 100644 erpnext/accounts/report/pos_register/pos_register.js create mode 100644 erpnext/accounts/report/pos_register/pos_register.json create mode 100644 erpnext/accounts/report/pos_register/pos_register.py diff --git a/erpnext/accounts/report/pos_register/__init__.py b/erpnext/accounts/report/pos_register/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/report/pos_register/pos_register.js b/erpnext/accounts/report/pos_register/pos_register.js new file mode 100644 index 0000000000..b8d48d92de --- /dev/null +++ b/erpnext/accounts/report/pos_register/pos_register.js @@ -0,0 +1,76 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["POS Register"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1, + "width": "60px" + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1, + "width": "60px" + }, + { + "fieldname":"pos_profile", + "label": __("POS Profile"), + "fieldtype": "Link", + "options": "POS Profile" + }, + { + "fieldname":"cashier", + "label": __("Cashier"), + "fieldtype": "Link", + "options": "User" + }, + { + "fieldname":"customer", + "label": __("Customer"), + "fieldtype": "Link", + "options": "Customer" + }, + { + "fieldname":"mode_of_payment", + "label": __("Payment Method"), + "fieldtype": "Link", + "options": "Mode of Payment" + }, + { + "fieldname":"group_by", + "label": __("Group by"), + "fieldtype": "Select", + "options": ["", "POS Profile", "Cashier", "Payment Method", "Customer"], + "default": "POS Profile" + }, + { + "fieldname":"is_return", + "label": __("Is Return"), + "fieldtype": "Check" + }, + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (data && data.bold) { + value = value.bold(); + + } + return value; + } +}; diff --git a/erpnext/accounts/report/pos_register/pos_register.json b/erpnext/accounts/report/pos_register/pos_register.json new file mode 100644 index 0000000000..2398b10475 --- /dev/null +++ b/erpnext/accounts/report/pos_register/pos_register.json @@ -0,0 +1,30 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2020-09-10 19:25:03.766871", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2020-09-10 19:25:15.851331", + "modified_by": "Administrator", + "module": "Accounts", + "name": "POS Register", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "POS Invoice", + "report_name": "POS Register", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py new file mode 100644 index 0000000000..0bcde64f7f --- /dev/null +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -0,0 +1,222 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _, _dict +from erpnext import get_company_currency, get_default_company +from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments + +def execute(filters=None): + if not filters: + return [], [] + + validate_filters(filters) + + columns = get_columns(filters) + + group_by_field = get_group_by_field(filters.get("group_by")) + + pos_entries = get_pos_entries(filters, group_by_field) + if group_by_field != "mode_of_payment": + concat_mode_of_payments(pos_entries) + + # return only entries if group by is unselected + if not group_by_field: + return columns, pos_entries + + # handle grouping + invoice_map, grouped_data = {}, [] + for d in pos_entries: + invoice_map.setdefault(d[group_by_field], []).append(d) + + for key in invoice_map: + invoices = invoice_map[key] + grouped_data += invoices + add_subtotal_row(grouped_data, invoices, group_by_field, key) + + # move group by column to first position + column_index = next((index for (index, d) in enumerate(columns) if d["fieldname"] == group_by_field), None) + columns.insert(0, columns.pop(column_index)) + + return columns, grouped_data + +def get_pos_entries(filters, group_by_field): + conditions = get_conditions(filters) + order_by = "p.posting_date" + select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", "" + if group_by_field == "mode_of_payment": + select_mop_field = ", sip.mode_of_payment" + from_sales_invoice_payment = ", `tabSales Invoice Payment` sip" + group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount, 0) != 0 AND" + order_by += ", sip.mode_of_payment" + + elif group_by_field: + order_by += ", p.{}".format(group_by_field) + + return frappe.db.sql( + """ + SELECT + p.posting_date, p.name as pos_invoice, p.pos_profile, + p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount, + p.customer, p.is_return {select_mop_field} + FROM + `tabPOS Invoice` p {from_sales_invoice_payment} + WHERE + {group_by_mop_condition} + {conditions} + ORDER BY + {order_by} + """.format( + select_mop_field=select_mop_field, + from_sales_invoice_payment=from_sales_invoice_payment, + group_by_mop_condition=group_by_mop_condition, + conditions=conditions, + order_by=order_by + ), filters, as_dict=1) + +def concat_mode_of_payments(pos_entries): + mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries])) + for entry in pos_entries: + if mode_of_payments.get(entry.pos_invoice): + entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, [])) + +def add_subtotal_row(data, group_invoices, group_by_field, group_by_value): + grand_total = sum([d.grand_total for d in group_invoices]) + paid_amount = sum([d.paid_amount for d in group_invoices]) + data.append({ + group_by_field: group_by_value, + "grand_total": grand_total, + "paid_amount": paid_amount, + "bold": 1 + }) + data.append({}) + +def validate_filters(filters): + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) + + if not filters.get("from_date") and not filters.get("to_date"): + frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) + + if filters.from_date > filters.to_date: + frappe.throw(_("From Date must be before To Date")) + + if (filters.get("pos_profile") and filters.get("group_by") == _('POS Profile')): + frappe.throw(_("Can not filter based on POS Profile, if grouped by POS Profile")) + + if (filters.get("customer") and filters.get("group_by") == _('Customer')): + frappe.throw(_("Can not filter based on Customer, if grouped by Customer")) + + if (filters.get("owner") and filters.get("group_by") == _('Cashier')): + frappe.throw(_("Can not filter based on Cashier, if grouped by Cashier")) + + if (filters.get("mode_of_payment") and filters.get("group_by") == _('Payment Method')): + frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method")) + +def get_conditions(filters): + conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s".format( + company=filters.get("company"), + from_date=filters.get("from_date"), + to_date=filters.get("to_date")) + + if filters.get("pos_profile"): + conditions += " AND pos_profile = %(pos_profile)s".format(pos_profile=filters.get("pos_profile")) + + if filters.get("owner"): + conditions += " AND owner = %(owner)s".format(owner=filters.get("owner")) + + if filters.get("customer"): + conditions += " AND customer = %(customer)s".format(customer=filters.get("customer")) + + if filters.get("is_return"): + conditions += " AND is_return = %(is_return)s".format(is_return=filters.get("is_return")) + + if filters.get("mode_of_payment"): + conditions += """ + AND EXISTS( + SELECT name FROM `tabSales Invoice Payment` sip + WHERE parent=p.name AND ifnull(sip.mode_of_payment, '') = %(mode_of_payment)s + )""" + + return conditions + +def get_group_by_field(group_by): + group_by_field = "" + + if group_by == "POS Profile": + group_by_field = "pos_profile" + elif group_by == "Cashier": + group_by_field = "owner" + elif group_by == "Customer": + group_by_field = "customer" + elif group_by == "Payment Method": + group_by_field = "mode_of_payment" + + return group_by_field + +def get_columns(filters): + columns = [ + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 90 + }, + { + "label": _("POS Invoice"), + "fieldname": "pos_invoice", + "fieldtype": "Link", + "options": "POS Invoice", + "width": 120 + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 120 + }, + { + "label": _("POS Profile"), + "fieldname": "pos_profile", + "fieldtype": "Link", + "options": "POS Profile", + "width": 160 + }, + { + "label": _("Cashier"), + "fieldname": "owner", + "fieldtype": "Link", + "options": "User", + "width": 140 + }, + { + "label": _("Grand Total"), + "fieldname": "grand_total", + "fieldtype": "Currency", + "options": "company:currency", + "width": 120 + }, + { + "label": _("Paid Amount"), + "fieldname": "paid_amount", + "fieldtype": "Currency", + "options": "company:currency", + "width": 120 + }, + { + "label": _("Payment Method"), + "fieldname": "mode_of_payment", + "fieldtype": "Data", + "width": 150 + }, + { + "label": _("Is Return"), + "fieldname": "is_return", + "fieldtype": "Data", + "width": 80 + }, + ] + + return columns \ No newline at end of file From ebb0a6266a2e13307342857c75982a979ac86fab Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Oct 2020 17:09:09 +0530 Subject: [PATCH 35/37] fix: last purchase rate in item prices report (#23506) * fix: last purchase rate in item prices report * fix: last purchase rate in item prices report * fix: last purchase rate in item prices report * chore: fetch last purchase rate from update stock purchase invoices --- .../stock/report/item_prices/item_prices.py | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index aa3ed92079..12f3297203 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -77,38 +77,33 @@ def get_price_list(): return item_rate_map def get_last_purchase_rate(): - item_last_purchase_rate_map = {} - query = """select * from (select - result.item_code, - result.base_rate - from ( - (select - po_item.item_code, - po_item.item_name, - po.transaction_date as posting_date, - po_item.base_price_list_rate, - po_item.discount_percentage, - po_item.base_rate - from `tabPurchase Order` po, `tabPurchase Order Item` po_item - where po.name = po_item.parent and po.docstatus = 1) - union - (select - pr_item.item_code, - pr_item.item_name, - pr.posting_date, - pr_item.base_price_list_rate, - pr_item.discount_percentage, - pr_item.base_rate - from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - where pr.name = pr_item.parent and pr.docstatus = 1) - ) result - order by result.item_code asc, result.posting_date desc) result_wrapper - group by item_code""" + query = """select * from ( + (select + po_item.item_code, + po.transaction_date as posting_date, + po_item.base_rate + from `tabPurchase Order` po, `tabPurchase Order Item` po_item + where po.name = po_item.parent and po.docstatus = 1) + union + (select + pr_item.item_code, + pr.posting_date, + pr_item.base_rate + from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item + where pr.name = pr_item.parent and pr.docstatus = 1) + union + (select + pi_item.item_code, + pi.posting_date, + pi_item.base_rate + from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item + where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1) + ) result order by result.item_code asc, result.posting_date asc""" for d in frappe.db.sql(query, as_dict=1): - item_last_purchase_rate_map.setdefault(d.item_code, d.base_rate) + item_last_purchase_rate_map[d.item_code] = d.base_rate return item_last_purchase_rate_map From b332c5f06f73057091aaa95fcc17a6f3cc5320db Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Oct 2020 17:10:37 +0530 Subject: [PATCH 36/37] fix: sales invoice series gets overwritten with pos invoice series (#23479) * fix: sales invoice series gets overwritten with pos invoice series * chore: show is consolidated only if sales invoice is pos --- .../accounts/doctype/pos_profile/pos_profile.js | 9 --------- .../doctype/pos_profile/pos_profile.json | 16 ++-------------- .../accounts/doctype/pos_profile/pos_profile.py | 4 ---- .../doctype/sales_invoice/sales_invoice.json | 1 + .../doctype/sales_invoice/sales_invoice.py | 2 +- 5 files changed, 4 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 8ec6a53626..558e21c13a 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -15,15 +15,6 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); - - frm.call({ - method: "erpnext.accounts.doctype.pos_profile.pos_profile.get_series", - callback: function(r) { - if(!r.exc) { - set_field_options("naming_series", r.message); - } - } - }); }); frappe.ui.form.on('POS Profile', { diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index d4c1791789..999da75997 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -8,7 +8,6 @@ "field_order": [ "disabled", "section_break_2", - "naming_series", "customer", "company", "country", @@ -59,17 +58,6 @@ "fieldname": "section_break_2", "fieldtype": "Section Break" }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Series", - "no_copy": 1, - "oldfieldname": "naming_series", - "oldfieldtype": "Select", - "options": "[Select]", - "reqd": 1 - }, { "fieldname": "customer", "fieldtype": "Link", @@ -323,7 +311,7 @@ "icon": "icon-cog", "idx": 1, "links": [], - "modified": "2020-06-29 12:20:30.977272", + "modified": "2020-10-01 17:29:27.759088", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", @@ -350,4 +338,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 1386b70f55..1d160a5aa7 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -109,10 +109,6 @@ def get_child_nodes(group_type, root): return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1) -@frappe.whitelist() -def get_series(): - return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s" - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index cd66f7a086..c7d791ef7a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1926,6 +1926,7 @@ }, { "default": "0", + "depends_on": "eval:(doc.is_pos && doc.is_consolidated)", "fieldname": "is_consolidated", "fieldtype": "Check", "label": "Is Consolidated", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 92e49d59da..206340b9a6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -428,7 +428,7 @@ class SalesInvoice(SellingController): if pos.get('account_for_change_amount'): self.account_for_change_amount = pos.get('account_for_change_amount') - for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name', + for fieldname in ('currency', 'letter_head', 'tc_name', 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', 'write_off_cost_center', 'apply_discount_on', 'cost_center'): if (not for_validate) or (for_validate and not self.get(fieldname)): From 3daad224adb02526d0276d7964054edd655aa0f5 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Oct 2020 18:31:23 +0530 Subject: [PATCH 37/37] fix: cannot merge pos invoice if validate selling price is checked (#23593) * fix: cannot merge pos invoice if validate selling price is checked * fix: validate selling price * fix: test * chore: add test * fix: error message --- .../doctype/pos_invoice/test_pos_invoice.py | 57 +++++++++++++++++-- erpnext/controllers/selling_controller.py | 22 ++++--- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 1a5920d8ab..e08af952dc 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -7,6 +7,7 @@ import frappe import unittest, copy, time from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPOSInvoice(unittest.TestCase): def test_timestamp_change(self): @@ -307,8 +308,9 @@ class TestPOSInvoice(unittest.TestCase): merge_pos_invoices() pos_inv.load_from_db() - sales_invoice = frappe.get_doc("Sales Invoice", pos_inv.consolidated_invoice) - self.assertEqual(sales_invoice.grand_total, 3500) + rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") + self.assertEqual(rounded_total, 3500) + frappe.set_user("Administrator") def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile @@ -348,8 +350,55 @@ class TestPOSInvoice(unittest.TestCase): merge_pos_invoices() pos_inv.load_from_db() - sales_invoice = frappe.get_doc("Sales Invoice", pos_inv.consolidated_invoice) - self.assertEqual(sales_invoice.rounded_total, 840) + rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") + self.assertEqual(rounded_total, 840) + frappe.set_user("Administrator") + + def test_merging_with_validate_selling_price(self): + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile + from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices + + if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): + frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1) + + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=1, basic_rate=300) + frappe.db.sql("delete from `tabPOS Invoice`") + test_user, pos_profile = init_user_and_profile() + pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 + }) + pos_inv.append('taxes', { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + 'included_in_print_rate': 1 + }) + self.assertRaises(frappe.ValidationError, pos_inv.submit) + + pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1) + pos_inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400 + }) + pos_inv2.append('taxes', { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + 'included_in_print_rate': 1 + }) + pos_inv2.submit() + + merge_pos_invoices() + + pos_inv2.load_from_db() + rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") + self.assertEqual(rounded_total, 400) + frappe.set_user("Administrator") + frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0) def create_pos_invoice(**args): args = frappe._dict(args) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 0ae175989e..58861715c2 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint, flt, cstr, comma_or +from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form from frappe import _, throw from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.utils import get_incoming_rate @@ -173,22 +173,26 @@ class SellingController(StockController): def validate_selling_price(self): def throw_message(idx, item_name, rate, ref_rate_field): - frappe.throw(_("""Row #{}: Selling rate for item {} is lower than its {}. Selling rate should be atleast {}""") - .format(idx, item_name, ref_rate_field, rate)) + bold_net_rate = frappe.bold("net rate") + msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""") + .format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate))) + msg += "

" + msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""") + .format(get_link_to_form("Selling Settings", "Selling Settings"))) + frappe.throw(msg, title=_("Invalid Selling Price")) if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): return - if hasattr(self, "is_return") and self.is_return: return for it in self.get("items"): if not it.item_code: continue - + last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) - last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1) - if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom): + last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) + if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom): throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate") last_valuation_rate = frappe.db.sql(""" @@ -197,8 +201,8 @@ class SellingController(StockController): ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1 """, (it.item_code, it.warehouse)) if last_valuation_rate: - last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1) - if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom) \ + last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1) + if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \ and not self.get('is_internal_customer'): throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")