From 610ead22e8f2b73321fa0f67cd030d35ca7f462a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 20 Jul 2023 13:08:26 +0530 Subject: [PATCH 01/60] fix: show only projects with access in customer portal --- erpnext/projects/doctype/project/project.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 7d80ac1cb7..f5fd67bfcf 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -13,6 +13,7 @@ from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime from erpnext import get_default_company from erpnext.controllers.queries import get_filters_cond +from erpnext.controllers.website_list_for_contact import get_customers_suppliers from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday @@ -318,6 +319,7 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]: def get_project_list( doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" ): + customers, suppliers = get_customers_suppliers("Project", frappe.session.user) meta = frappe.get_meta(doctype) if not filters: filters = [] @@ -325,6 +327,7 @@ def get_project_list( fields = "distinct *" or_filters = [] + filters.append([doctype, "customer", "in", customers]) if txt: if meta.search_fields: From 5d7dd9b0ec5759bda695e2fb3dcd6527a769505a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 20 Jul 2023 18:20:53 +0530 Subject: [PATCH 02/60] fix: project route permissions for user --- erpnext/hooks.py | 3 +- erpnext/projects/doctype/project/project.py | 40 +++++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 316d9437fb..dab166e49f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -185,7 +185,7 @@ website_route_rules = [ ] standard_portal_menu_items = [ - {"title": "Projects", "route": "/project", "reference_doctype": "Project"}, + {"title": "Projects", "route": "/project", "reference_doctype": "Project", "role": "Customer"}, { "title": "Request for Quotations", "route": "/rfq", @@ -277,6 +277,7 @@ has_website_permission = { "Delivery Note": "erpnext.controllers.website_list_for_contact.has_website_permission", "Issue": "erpnext.support.doctype.issue.issue.has_website_permission", "Timesheet": "erpnext.controllers.website_list_for_contact.has_website_permission", + "Project": "erpnext.controllers.website_list_for_contact.has_website_permission", } before_tests = "erpnext.setup.utils.before_tests" diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index f5fd67bfcf..c2ed579e73 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -10,6 +10,7 @@ from frappe.model.document import Document from frappe.query_builder import Interval from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today +from frappe.utils.user import is_website_user from erpnext import get_default_company from erpnext.controllers.queries import get_filters_cond @@ -319,15 +320,24 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]: def get_project_list( doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" ): + user = frappe.session.user customers, suppliers = get_customers_suppliers("Project", frappe.session.user) + + ignore_permissions = False + if is_website_user(): + if not filters: + filters = [] + + if customers: + filters.append([doctype, "customer", "in", customers]) + + ignore_permissions = True + meta = frappe.get_meta(doctype) - if not filters: - filters = [] fields = "distinct *" or_filters = [] - filters.append([doctype, "customer", "in", customers]) if txt: if meta.search_fields: @@ -354,18 +364,26 @@ def get_project_list( limit_start=limit_start, limit_page_length=limit_page_length, order_by=order_by, + ignore_permissions=ignore_permissions, ) def get_list_context(context=None): - return { - "show_sidebar": True, - "show_search": True, - "no_breadcrumbs": True, - "title": _("Projects"), - "get_list": get_project_list, - "row_template": "templates/includes/projects/project_row.html", - } + from erpnext.controllers.website_list_for_contact import get_list_context + + list_context = get_list_context(context) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Projects"), + "get_list": get_project_list, + "row_template": "templates/includes/projects/project_row.html", + } + ) + + return list_context @frappe.whitelist() From 7e344685043bfba06ed4c74f7c3b006d87ffaa9c Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:27:18 +0530 Subject: [PATCH 03/60] fix: POS Invoice Email Receipt Mail --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index be75bd64cf..cfad587098 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -49,7 +49,7 @@ erpnext.PointOfSale.PastOrderSummary = class { title: 'Email Receipt', fields: [ {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'}, - // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'} + {fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'} ], primary_action: () => { this.send_email(); @@ -243,6 +243,7 @@ erpnext.PointOfSale.PastOrderSummary = class { send_email() { const frm = this.events.get_frm(); const recipients = this.email_dialog.get_values().email_id; + const content = this.email_dialog.get_values().content; const doc = this.doc || frm.doc; const print_format = frm.pos_print_format; @@ -251,6 +252,7 @@ erpnext.PointOfSale.PastOrderSummary = class { args: { recipients: recipients, subject: __(frm.meta.name) + ': ' + doc.name, + content: content ? content : __(frm.meta.name) + ': ' + doc.name, doctype: doc.doctype, name: doc.name, send_email: 1, From dd91a77fdd3ea6ed88d9afd02285bfe38e90a717 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:19:09 +0530 Subject: [PATCH 04/60] fix: POS Invoice Email Receipt Mail --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index cfad587098..d341d23bd3 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -48,7 +48,7 @@ erpnext.PointOfSale.PastOrderSummary = class { const email_dialog = new frappe.ui.Dialog({ title: 'Email Receipt', fields: [ - {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'}, + {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID', reqd: 1}, {fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'} ], primary_action: () => { From c071523e3518c2511bcadce5b109de668ae58835 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 16 Aug 2023 12:23:10 +0530 Subject: [PATCH 05/60] fix: delete portal settings for education doctypes --- erpnext/patches/v14_0/delete_education_doctypes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py index 76b2300fd2..24b9fa1d71 100644 --- a/erpnext/patches/v14_0/delete_education_doctypes.py +++ b/erpnext/patches/v14_0/delete_education_doctypes.py @@ -44,6 +44,9 @@ def execute(): doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name") for doctype in doctypes: + frappe.delete_doc( + "Portal Menu Item", {"reference_doctype": doctype}, ignore_missing=True, force=True + ) frappe.delete_doc("DocType", doctype, ignore_missing=True) frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True) From d119d2ec320e7f7c733253e1850c81c3f209bfd6 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 16 Aug 2023 17:35:07 +0530 Subject: [PATCH 06/60] fix: delete doc without filters --- erpnext/patches/v14_0/delete_education_doctypes.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py index 24b9fa1d71..468c2fda32 100644 --- a/erpnext/patches/v14_0/delete_education_doctypes.py +++ b/erpnext/patches/v14_0/delete_education_doctypes.py @@ -44,11 +44,14 @@ def execute(): doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name") for doctype in doctypes: - frappe.delete_doc( - "Portal Menu Item", {"reference_doctype": doctype}, ignore_missing=True, force=True - ) frappe.delete_doc("DocType", doctype, ignore_missing=True) + portal_menu_items = frappe.get_all( + "Portal Menu Item", {"reference_doctype": ("in", doctypes)}, pluck="name" + ) + for menu_item in portal_menu_items: + frappe.delete_doc("Portal Menu Item", menu_item, ignore_missing=True, force=True) + frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True) click.secho( From e8f6c286d1c3eeff2851da84d8b250bc922d3254 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 13:21:37 +0530 Subject: [PATCH 07/60] fix: remove portal menu items in pre-model sync patch --- erpnext/patches.txt | 1 + .../delete_education_module_portal_menu_items.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 erpnext/patches/v14_0/delete_education_module_portal_menu_items.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 73e0a95da9..76e4dee9b6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -262,6 +262,7 @@ erpnext.patches.v14_0.update_reference_due_date_in_journal_entry erpnext.patches.v15_0.saudi_depreciation_warning erpnext.patches.v15_0.delete_saudi_doctypes erpnext.patches.v14_0.show_loan_management_deprecation_warning +erpnext.patches.v14_0.delete_education_module_portal_menu_items [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') diff --git a/erpnext/patches/v14_0/delete_education_module_portal_menu_items.py b/erpnext/patches/v14_0/delete_education_module_portal_menu_items.py new file mode 100644 index 0000000000..d964f14944 --- /dev/null +++ b/erpnext/patches/v14_0/delete_education_module_portal_menu_items.py @@ -0,0 +1,13 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + +import frappe + + +def execute(): + doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name") + items = frappe.get_all( + "Portal Menu Item", filters={"reference_doctype": ("in", doctypes)}, pluck="name" + ) + for item in items: + frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True) From 8010a157b185d3ce299bc4c9a9c3b44dd360acae Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 11:06:19 +0530 Subject: [PATCH 08/60] fix: use `flt` for qty and rate fields --- .../subcontracting_receipt/subcontracting_receipt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index d2bf7e8f1d..53c567af6b 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -183,9 +183,9 @@ class SubcontractingReceipt(SubcontractingController): flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) ) - item.received_qty = item.qty + flt(item.rejected_qty) - item.amount = item.qty * item.rate - total_qty += item.qty + item.received_qty = flt(item.qty) + flt(item.rejected_qty) + item.amount = flt(item.qty) * flt(item.rate) + total_qty += flt(item.qty) total_amount += item.amount else: self.total_qty = total_qty From 6d9cebfee94a3485b207bbd5d298a680c26addef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 11:49:03 +0530 Subject: [PATCH 09/60] fix: Tax withholding reversal on Debit Notes --- .../tax_withholding_category/tax_withholding_category.py | 4 ++-- erpnext/controllers/sales_and_purchase_return.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 954b4e7957..de2f9e7e0d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -271,9 +271,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details ) else: - tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + tax_amount = net_total * tax_details.rate / 100 else: - tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + tax_amount = net_total * tax_details.rate / 100 # once tds is deducted, not need to add vouchers in the invoice voucher_wise_amount = {} diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 173e812dbd..165e17b2d7 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -345,6 +345,8 @@ def make_return_doc( elif doctype == "Purchase Invoice": # look for Print Heading "Debit Note" doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note")) + if source.tax_withholding_category: + doc.set_onload("supplier_tds", source.tax_withholding_category) for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": From 4e7cccbdf08c9be6df0c63bad47797351a53c54b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 11:47:11 +0530 Subject: [PATCH 10/60] feat: `Is Scrap Item` field in Subcontracting Receipt Item --- .../subcontracting_receipt_item.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index d72878061c..fc09422306 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -10,6 +10,7 @@ "item_code", "column_break_2", "item_name", + "is_scrap_item", "section_break_4", "description", "brand", @@ -345,7 +346,8 @@ "label": "BOM", "no_copy": 1, "options": "BOM", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval: doc.is_scrap_item" }, { "fetch_from": "item_code.brand", @@ -490,12 +492,22 @@ "no_copy": 1, "options": "Serial and Batch Bundle", "print_hide": 1 + }, + { + "default": "0", + "depends_on": "eval: !doc.bom", + "fieldname": "is_scrap_item", + "fieldtype": "Check", + "label": "Is Scrap Item", + "no_copy": 1, + "print_hide": 1, + "read_only_depends_on": "eval: doc.bom" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-06 18:43:45.599761", + "modified": "2023-08-24 11:52:22.666506", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From d1e877b6f085399ac24192d5d0513800e580efd3 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 12:16:12 +0530 Subject: [PATCH 11/60] refactor: Subcontracting Receipt Item form view --- .../subcontracting_receipt_item.json | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index fc09422306..b6f1155ce3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -25,8 +25,6 @@ "col_break2", "stock_uom", "conversion_factor", - "tracking_section", - "col_break_tracking_section", "rate_and_amount", "rate", "amount", @@ -38,15 +36,15 @@ "rm_supp_cost", "warehouse_and_reference", "warehouse", - "rejected_warehouse", "subcontracting_order", - "column_break_40", - "schedule_date", - "quality_inspection", "subcontracting_order_item", "subcontracting_receipt_item", - "section_break_45", + "column_break_40", + "rejected_warehouse", "bom", + "quality_inspection", + "schedule_date", + "section_break_45", "serial_and_batch_bundle", "serial_no", "col_break5", @@ -86,12 +84,13 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, "label": "Item Name", - "print_hide": 1, - "reqd": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -100,11 +99,12 @@ "label": "Description" }, { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Text Editor", "label": "Description", "print_width": "300px", - "reqd": 1, "width": "300px" }, { @@ -296,7 +296,8 @@ }, { "fieldname": "section_break_45", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Serial and Batch Details" }, { "depends_on": "eval:!doc.is_fixed_asset", @@ -322,7 +323,8 @@ "fieldtype": "Small Text", "label": "Rejected Serial No", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "subcontracting_order_item", @@ -412,14 +414,6 @@ "fieldname": "image_column", "fieldtype": "Column Break" }, - { - "fieldname": "tracking_section", - "fieldtype": "Section Break" - }, - { - "fieldname": "col_break_tracking_section", - "fieldtype": "Column Break" - }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", @@ -507,7 +501,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-08-24 11:52:22.666506", + "modified": "2023-08-24 12:14:25.123857", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From 8f1e00906fde550f3c49111f0015f7fe6241e275 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 24 Aug 2023 12:28:11 +0530 Subject: [PATCH 12/60] fix: fetch JVs with no party selected in filters --- .../tax_withholding_details.py | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 7d16661472..7191720c57 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -257,7 +257,7 @@ def get_tds_docs(filters): } party = frappe.get_all(filters.get("party_type"), pluck="name") - query_filters.update({"against": ("in", party)}) + or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"}) if filters.get("party"): del query_filters["account"] @@ -294,7 +294,7 @@ def get_tds_docs(filters): if journal_entries: journal_entry_party_map = get_journal_entry_party_map(journal_entries) - get_doc_info(journal_entries, "Journal Entry", tax_category_map) + get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map) return ( tds_documents, @@ -309,7 +309,11 @@ def get_journal_entry_party_map(journal_entries): journal_entry_party_map = {} for d in frappe.db.get_all( "Journal Entry Account", - {"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")}, + { + "parent": ("in", journal_entries), + "party_type": ("in", ("Supplier", "Customer")), + "party": ("is", "set"), + }, ["parent", "party"], ): if d.parent not in journal_entry_party_map: @@ -320,41 +324,29 @@ def get_journal_entry_party_map(journal_entries): def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): - if doctype == "Purchase Invoice": - fields = [ - "name", - "tax_withholding_category", - "base_tax_withholding_net_total", - "grand_total", - "base_total", - ] - elif doctype == "Sales Invoice": - fields = ["name", "base_net_total", "grand_total", "base_total"] - elif doctype == "Payment Entry": - fields = [ - "name", - "tax_withholding_category", - "paid_amount", - "paid_amount_after_tax", - "base_paid_amount", - ] - else: - fields = ["name", "tax_withholding_category"] + common_fields = ["name", "tax_withholding_category"] + fields_dict = { + "Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"], + "Sales Invoice": ["base_net_total", "grand_total", "base_total"], + "Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"], + "Journal Entry": ["total_amount"], + } - entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields) + entries = frappe.get_all( + doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype] + ) for entry in entries: tax_category_map.update({entry.name: entry.tax_withholding_category}) if doctype == "Purchase Invoice": - net_total_map.update( - {entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]} - ) + value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total] elif doctype == "Sales Invoice": - net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]}) + value = [entry.base_net_total, entry.grand_total, entry.base_total] elif doctype == "Payment Entry": - net_total_map.update( - {entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]} - ) + value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount] + else: + value = [entry.total_amount] * 3 + net_total_map.update({entry.name: value}) def get_tax_rate_map(filters): From 7c1417e19913ded3424bb940f770480898a72771 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 24 Aug 2023 12:55:54 +0530 Subject: [PATCH 13/60] chore: fix linting issues --- erpnext/accounts/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d6d0f91fb..1aefeaacf7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,9 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering - limit=None, # passed by reconciliation tool - voucher_no=None, # filter passed by reconciliation tool + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") From a33b75f2cd7ee4512361d8e1b7bb54eaadde551b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 16:37:57 +0530 Subject: [PATCH 14/60] feat: allow manually entry for scrap items in SCR --- erpnext/controllers/buying_controller.py | 18 ------- .../controllers/subcontracting_controller.py | 51 +++++++++++++------ .../subcontracting_receipt.py | 36 ++++++------- 3 files changed, 54 insertions(+), 51 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index b396b27da7..b1ce539bc3 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -436,24 +436,6 @@ class BuyingController(SubcontractingController): # validate rate with ref PR - def validate_rejected_warehouse(self): - for item in self.get("items"): - if flt(item.rejected_qty) and not item.rejected_warehouse: - if self.rejected_warehouse: - item.rejected_warehouse = self.rejected_warehouse - - if not item.rejected_warehouse: - frappe.throw( - _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format( - item.idx, item.item_code - ) - ) - - if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")): - frappe.throw( - _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) - ) - # validate accepted and rejected qty def validate_accepted_rejected_qty(self): for d in self.get("items"): diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 6633f4f6eb..837969728b 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -55,6 +55,23 @@ class SubcontractingController(StockController): else: super(SubcontractingController, self).validate() + def validate_rejected_warehouse(self): + for item in self.get("items"): + if flt(item.rejected_qty) and not item.rejected_warehouse: + if self.rejected_warehouse: + item.rejected_warehouse = self.rejected_warehouse + else: + frappe.throw( + _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format( + item.idx, item.item_code + ) + ) + + if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")): + frappe.throw( + _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) + ) + def remove_empty_rows(self): for key in ["service_items", "items", "supplied_items"]: if self.get(key): @@ -80,23 +97,27 @@ class SubcontractingController(StockController): if not is_stock_item: frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name)) - if not is_sub_contracted_item: - frappe.throw( - _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) - ) + if not item.is_scrap_item: + if not is_sub_contracted_item: + frappe.throw( + _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) + ) - if item.bom: - bom = frappe.get_doc("BOM", item.bom) - if not bom.is_active: - frappe.throw( - _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name) - ) - if bom.item != item.item_code: - frappe.throw( - _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name) - ) + if item.bom: + is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"]) + + if not is_active: + frappe.throw( + _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name) + ) + if bom_item != item.item_code: + frappe.throw( + _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name) + ) + else: + frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name)) else: - frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name)) + item.bom = None def __get_data_before_save(self): item_dict = {} diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 53c567af6b..b1a585d147 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -79,6 +79,7 @@ class SubcontractingReceipt(SubcontractingController): super(SubcontractingReceipt, self).validate() self.set_missing_values() self.validate_posting_time() + self.validate_accepted_warehouse() self.validate_rejected_warehouse() if getdate(self.posting_date) > getdate(nowdate()): @@ -127,6 +128,23 @@ class SubcontractingReceipt(SubcontractingController): self.calculate_supplied_items_qty_and_amount() self.calculate_items_qty_and_amount() + def validate_accepted_warehouse(self): + for item in self.get("items"): + if flt(item.qty) and not item.warehouse: + if self.set_warehouse: + item.warehouse = self.set_warehouse + else: + frappe.throw( + _("Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}").format( + item.idx, item.item_code + ) + ) + + if item.get("warehouse") and (item.get("warehouse") == item.get("rejected_warehouse")): + frappe.throw( + _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) + ) + def set_available_qty_for_consumption(self): supplied_items_details = {} @@ -191,24 +209,6 @@ class SubcontractingReceipt(SubcontractingController): self.total_qty = total_qty self.total = total_amount - def validate_rejected_warehouse(self): - for item in self.items: - if flt(item.rejected_qty) and not item.rejected_warehouse: - if self.rejected_warehouse: - item.rejected_warehouse = self.rejected_warehouse - - if not item.rejected_warehouse: - frappe.throw( - _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format( - item.idx, item.item_code - ) - ) - - if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")): - frappe.throw( - _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) - ) - def validate_available_qty_for_consumption(self): for item in self.get("supplied_items"): precision = item.precision("consumed_qty") From 3c15feadf66c1629e9bc373b28d435c7cc2b1825 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 24 Aug 2023 17:24:44 +0530 Subject: [PATCH 15/60] feat: Multi-level BOM Creator (#36494) * feat: Multi-level BOM Creator * fix: renamed BOM Configurator to BOM Creator * fix: added Cost in the tree * fix: finished good cost * fix: valuation rate in tree ui * chore: conflicts and removed unnecessary files * test: test cases for BOM Creator * fix: added shortcut for the BOM Creator * fix: added validation for Final Product --- erpnext/manufacturing/doctype/bom/bom.json | 34 +- erpnext/manufacturing/doctype/bom/bom.py | 50 ++- .../doctype/bom_creator/__init__.py | 0 .../doctype/bom_creator/bom_creator.js | 201 +++++++++ .../doctype/bom_creator/bom_creator.json | 330 ++++++++++++++ .../doctype/bom_creator/bom_creator.py | 424 ++++++++++++++++++ .../doctype/bom_creator/bom_creator_list.js | 18 + .../doctype/bom_creator/test_bom_creator.py | 240 ++++++++++ .../doctype/bom_creator_item/__init__.py | 0 .../bom_creator_item/bom_creator_item.json | 243 ++++++++++ .../bom_creator_item/bom_creator_item.py | 9 + .../manufacturing/manufacturing.json | 11 +- .../bom_configurator.bundle.js | 416 +++++++++++++++++ erpnext/public/js/utils.js | 4 + 14 files changed, 1963 insertions(+), 17 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_creator/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator.js create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator.json create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator.py create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js create mode 100644 erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py create mode 100644 erpnext/public/js/bom_configurator/bom_configurator.bundle.js diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index d02402299e..e8d3542835 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -78,6 +78,10 @@ "show_items", "show_operations", "web_long_description", + "reference_section", + "bom_creator", + "bom_creator_item", + "column_break_oxbz", "amended_from", "connections_tab" ], @@ -233,7 +237,7 @@ "fieldname": "rm_cost_as_per", "fieldtype": "Select", "label": "Rate Of Materials Based On", - "options": "Valuation Rate\nLast Purchase Rate\nPrice List" + "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual" }, { "allow_on_submit": 1, @@ -599,6 +603,32 @@ "fieldname": "operating_cost_per_bom_quantity", "fieldtype": "Currency", "label": "Operating Cost Per BOM Quantity" + }, + { + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "bom_creator", + "fieldtype": "Link", + "label": "BOM Creator", + "no_copy": 1, + "options": "BOM Creator", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "bom_creator_item", + "fieldtype": "Data", + "label": "BOM Creator Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_oxbz", + "fieldtype": "Column Break" } ], "icon": "fa fa-sitemap", @@ -606,7 +636,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:47:58.514795", + "modified": "2023-08-07 11:38:08.152294", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8058a5f8b7..023166849d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -206,6 +206,7 @@ class BOM(WebsiteGenerator): def on_submit(self): self.manage_default_bom() + self.update_bom_creator_status() def on_cancel(self): self.db_set("is_active", 0) @@ -214,6 +215,23 @@ class BOM(WebsiteGenerator): # check if used in any other bom self.validate_bom_links() self.manage_default_bom() + self.update_bom_creator_status() + + def update_bom_creator_status(self): + if not self.bom_creator: + return + + if self.bom_creator_item: + frappe.db.set_value( + "BOM Creator Item", + self.bom_creator_item, + "bom_created", + 1 if self.docstatus == 1 else 0, + update_modified=False, + ) + + doc = frappe.get_doc("BOM Creator", self.bom_creator) + doc.set_status(save=True) def on_update_after_submit(self): self.validate_bom_links() @@ -662,18 +680,19 @@ class BOM(WebsiteGenerator): for d in self.get("items"): old_rate = d.rate - d.rate = self.get_rm_rate( - { - "company": self.company, - "item_code": d.item_code, - "bom_no": d.bom_no, - "qty": d.qty, - "uom": d.uom, - "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier, - } - ) + if self.rm_cost_as_per != "Manual": + d.rate = self.get_rm_rate( + { + "company": self.company, + "item_code": d.item_code, + "bom_no": d.bom_no, + "qty": d.qty, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "sourced_by_supplier": d.sourced_by_supplier, + } + ) d.base_rate = flt(d.rate) * flt(self.conversion_rate) d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) @@ -964,7 +983,12 @@ def get_valuation_rate(data): .as_("valuation_rate") ) .where((bin_table.item_code == item_code) & (wh_table.company == company)) - ).run(as_dict=True)[0] + ) + + if data.get("set_rate_based_on_warehouse") and data.get("warehouse"): + item_valuation = item_valuation.where(bin_table.warehouse == data.get("warehouse")) + + item_valuation = item_valuation.run(as_dict=True)[0] valuation_rate = item_valuation.get("valuation_rate") diff --git a/erpnext/manufacturing/doctype/bom_creator/__init__.py b/erpnext/manufacturing/doctype/bom_creator/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js new file mode 100644 index 0000000000..01dc89b080 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -0,0 +1,201 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.provide("erpnext.bom"); + +frappe.ui.form.on("BOM Creator", { + setup(frm) { + frm.trigger("set_queries"); + }, + + setup_bom_creator(frm) { + frm.dashboard.clear_comment(); + + if (!frm.is_new()) { + if ((!frappe.bom_configurator + || frappe.bom_configurator.bom_configurator !== frm.doc.name)) { + frm.trigger("build_tree"); + } + } else { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + frm.trigger("make_new_entry"); + } + }, + + build_tree(frm) { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + frm.toggle_enable("item_code", false); + + frappe.require('bom_configurator.bundle.js').then(() => { + frappe.bom_configurator = new frappe.ui.BOMConfigurator({ + wrapper: $parent, + page: $parent, + frm: frm, + bom_configurator: frm.doc.name, + }); + }); + }, + + make_new_entry(frm) { + let dialog = new frappe.ui.Dialog({ + title: __("Multi-level BOM Creator"), + fields: [ + { + label: __("Name"), + fieldtype: "Data", + fieldname: "name", + reqd: 1 + }, + { fieldtype: "Column Break" }, + { + label: __("Company"), + fieldtype: "Link", + fieldname: "company", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + { fieldtype: "Section Break" }, + { + label: __("Item Code (Final Product)"), + fieldtype: "Link", + fieldname: "item_code", + options: "Item", + reqd: 1 + }, + { fieldtype: "Column Break" }, + { + label: __("Quantity"), + fieldtype: "Float", + fieldname: "qty", + reqd: 1, + default: 1.0 + }, + { fieldtype: "Section Break" }, + { + label: __("Currency"), + fieldtype: "Link", + fieldname: "currency", + options: "Currency", + reqd: 1, + default: frappe.defaults.get_global_default("currency") + }, + { fieldtype: "Column Break" }, + { + label: __("Conversion Rate"), + fieldtype: "Float", + fieldname: "conversion_rate", + reqd: 1, + default: 1.0 + }, + ], + primary_action_label: __("Create"), + primary_action: (values) => { + values.doctype = frm.doc.doctype; + frappe.db + .insert(values) + .then((doc) => { + frappe.set_route("Form", doc.doctype, doc.name); + }); + } + }) + + dialog.show(); + }, + + set_queries(frm) { + frm.set_query("bom_no", "items", function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + return { + filters: { + item: item.item_code, + } + } + }); + }, + + refresh(frm) { + frm.trigger("setup_bom_creator"); + frm.trigger("set_root_item"); + frm.trigger("add_custom_buttons"); + }, + + set_root_item(frm) { + if (frm.is_new() && frm.doc.items?.length) { + frappe.model.set_value(frm.doc.items[0].doctype, + frm.doc.items[0].name, "is_root", 1); + } + }, + + add_custom_buttons(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__("Rebuild Tree"), () => { + frm.trigger("build_tree"); + }); + } + } +}); + +frappe.ui.form.on("BOM Creator Item", { + item_code(frm, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + if (item.item_code && item.is_root) { + frappe.model.set_value(cdt, cdn, "fg_item", item.item_code); + } + }, + + do_not_explode(frm, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + if (!item.do_not_explode) { + frm.call({ + method: "get_default_bom", + doc: frm.doc, + args: { + item_code: item.item_code + }, + callback(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, "bom_no", r.message); + } + } + }) + } else { + frappe.model.set_value(cdt, cdn, "bom_no", ""); + } + } +}); + + +erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionController { + conversion_rate(doc) { + if(this.frm.doc.currency === this.get_company_currency()) { + this.frm.set_value("conversion_rate", 1.0); + } else { + erpnext.bom.update_cost(doc); + } + } + + buying_price_list(doc) { + this.apply_price_list(); + } + + plc_conversion_rate(doc) { + if (!this.in_apply_price_list) { + this.apply_price_list(null, true); + } + } + + conversion_factor(doc, cdt, cdn) { + if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { + var item = frappe.get_doc(cdt, cdn); + frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); + item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); + refresh_field("stock_qty", item.name, item.parentfield); + this.toggle_conversion_factor(item); + this.frm.events.update_cost(this.frm); + } + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm})); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json new file mode 100644 index 0000000000..fb4c6c5c95 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -0,0 +1,330 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "prompt", + "creation": "2023-07-18 14:56:34.477800", + "default_view": "List", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "tab_2_tab", + "bom_creator", + "details_tab", + "section_break_ylsl", + "item_code", + "item_name", + "item_group", + "column_break_ikj7", + "qty", + "project", + "uom", + "raw_materials_tab", + "currency_detail", + "rm_cost_as_per", + "set_rate_based_on_warehouse", + "buying_price_list", + "price_list_currency", + "plc_conversion_rate", + "column_break_ivyw", + "currency", + "conversion_rate", + "section_break_zcfg", + "default_warehouse", + "column_break_tzot", + "company", + "materials_section", + "items", + "costing_detail", + "raw_material_cost", + "remarks_tab", + "remarks", + "section_break_yixm", + "status", + "column_break_irab", + "error_log", + "connections_tab", + "amended_from" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "fieldname": "currency_detail", + "fieldtype": "Section Break", + "label": "Costing" + }, + { + "allow_on_submit": 1, + "default": "Valuation Rate", + "fieldname": "rm_cost_as_per", + "fieldtype": "Select", + "label": "Rate Of Materials Based On", + "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per===\"Price List\"", + "fieldname": "buying_price_list", + "fieldtype": "Link", + "label": "Price List", + "options": "Price List" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "price_list_currency", + "fieldtype": "Link", + "label": "Price List Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "plc_conversion_rate", + "fieldtype": "Float", + "label": "Price List Exchange Rate" + }, + { + "fieldname": "column_break_ivyw", + "fieldtype": "Column Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "conversion_rate", + "fieldtype": "Float", + "label": "Conversion Rate", + "precision": "9" + }, + { + "fieldname": "materials_section", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, + { + "allow_bulk_edit": 1, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "bom_materials", + "oldfieldtype": "Table", + "options": "BOM Creator Item" + }, + { + "fieldname": "costing_detail", + "fieldtype": "Section Break", + "label": "Costing Details" + }, + { + "fieldname": "raw_material_cost", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Cost", + "no_copy": 1, + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "remarks", + "fieldtype": "Text Editor", + "label": "Remarks" + }, + { + "fieldname": "column_break_ikj7", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Finished Good", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "label": "Quantity", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "tab_2_tab", + "fieldtype": "Tab Break", + "label": "BOM Tree" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Final Product" + }, + { + "fieldname": "raw_materials_tab", + "fieldtype": "Tab Break", + "label": "Sub Assemblies & Raw Materials" + }, + { + "fieldname": "remarks_tab", + "fieldtype": "Tab Break", + "label": "Remarks" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "BOM Creator", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_zcfg", + "fieldtype": "Section Break", + "label": "Warehouse" + }, + { + "fieldname": "column_break_tzot", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_warehouse", + "fieldtype": "Link", + "label": "Default Source Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "bom_creator", + "fieldtype": "HTML" + }, + { + "fieldname": "section_break_ylsl", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.rm_cost_as_per === \"Valuation Rate\"", + "fieldname": "set_rate_based_on_warehouse", + "fieldtype": "Check", + "label": "Set Valuation Rate Based on Source Warehouse" + }, + { + "fieldname": "section_break_yixm", + "fieldtype": "Section Break" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Draft\nSubmitted\nIn Progress\nCompleted\nFailed\nCancelled", + "read_only": 1 + }, + { + "fieldname": "column_break_irab", + "fieldtype": "Column Break" + }, + { + "fieldname": "error_log", + "fieldtype": "Text", + "label": "Error Log", + "read_only": 1 + } + ], + "icon": "fa fa-sitemap", + "is_submittable": 1, + "links": [ + { + "link_doctype": "BOM", + "link_fieldname": "bom_creator" + } + ], + "modified": "2023-08-07 15:45:06.176313", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Creator", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py new file mode 100644 index 0000000000..999d610dfa --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -0,0 +1,424 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import OrderedDict + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import flt + +from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate + +BOM_FIELDS = [ + "company", + "rm_cost_as_per", + "project", + "currency", + "conversion_rate", + "buying_price_list", +] + +BOM_ITEM_FIELDS = [ + "item_code", + "qty", + "uom", + "rate", + "stock_qty", + "stock_uom", + "conversion_factor", + "do_not_explode", +] + + +class BOMCreator(Document): + def before_save(self): + self.set_status() + self.set_is_expandable() + self.set_conversion_factor() + self.set_reference_id() + self.set_rate_for_items() + + def validate(self): + self.validate_items() + + def validate_items(self): + for row in self.items: + if row.is_expandable and row.item_code == self.item_code: + frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code)) + + def set_status(self, save=False): + self.status = { + 0: "Draft", + 1: "Submitted", + 2: "Cancelled", + }[self.docstatus] + + self.set_status_completed() + if save: + self.db_set("status", self.status) + + def set_status_completed(self): + if self.docstatus != 1: + return + + has_completed = True + for row in self.items: + if row.is_expandable and not row.bom_created: + has_completed = False + break + + if not frappe.get_cached_value( + "BOM", {"bom_creator": self.name, "item": self.item_code}, "name" + ): + has_completed = False + + if has_completed: + self.status = "Completed" + + def on_cancel(self): + self.set_status(True) + + def set_conversion_factor(self): + for row in self.items: + row.conversion_factor = 1.0 + + def before_submit(self): + self.validate_fields() + self.set_status() + + def set_reference_id(self): + parent_reference = {row.idx: row.name for row in self.items} + + for row in self.items: + if row.fg_reference_id: + continue + + if row.parent_row_no: + row.fg_reference_id = parent_reference.get(row.parent_row_no) + + @frappe.whitelist() + def add_boms(self): + self.submit() + + def set_rate_for_items(self): + if self.rm_cost_as_per == "Manual": + return + + amount = self.get_raw_material_cost() + self.raw_material_cost = amount + + def get_raw_material_cost(self, fg_reference_id=None, amount=0): + if not fg_reference_id: + fg_reference_id = self.name + + for row in self.items: + if row.fg_reference_id != fg_reference_id: + continue + + if not row.is_expandable: + row.rate = get_bom_item_rate( + { + "company": self.company, + "item_code": row.item_code, + "bom_no": "", + "qty": row.qty, + "uom": row.uom, + "stock_uom": row.stock_uom, + "conversion_factor": row.conversion_factor, + "sourced_by_supplier": row.sourced_by_supplier, + }, + self, + ) + + row.amount = flt(row.rate) * flt(row.qty) + + else: + row.amount = 0.0 + row.amount = self.get_raw_material_cost(row.name, row.amount) + row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor)) + + amount += flt(row.amount) + + return amount + + def set_is_expandable(self): + fg_items = [row.fg_item for row in self.items if row.fg_item != self.item_code] + for row in self.items: + row.is_expandable = 0 + if row.item_code in fg_items: + row.is_expandable = 1 + + def validate_fields(self): + fields = { + "items": "Items", + } + + for field, label in fields.items(): + if not self.get(field): + frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name)) + + def on_submit(self): + self.enqueue_create_boms() + + def enqueue_create_boms(self): + frappe.enqueue( + self.create_boms, + queue="short", + timeout=600, + is_async=True, + ) + + frappe.msgprint( + _("BOMs creation has been enqueued, kindly check the status after some time"), alert=True + ) + + def create_boms(self): + """ + Sample data structure of production_item_wise_rm + production_item_wise_rm = { + (fg_item_code, name): { + "items": [], + "bom_no": "", + "fg_item_data": {} + } + } + """ + + self.db_set("status", "In Progress") + production_item_wise_rm = OrderedDict({}) + production_item_wise_rm.setdefault( + (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self}) + ) + + for row in self.items: + if row.is_expandable: + if (row.item_code, row.name) not in production_item_wise_rm: + production_item_wise_rm.setdefault( + (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}) + ) + + production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row) + + reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) + + try: + for d in reverse_tree: + fg_item_data = production_item_wise_rm.get(d).fg_item_data + self.create_bom(fg_item_data, production_item_wise_rm) + + frappe.msgprint(_("BOMs created successfully")) + except Exception: + traceback = frappe.get_traceback() + self.db_set( + { + "status": "Failed", + "error_log": traceback, + } + ) + + frappe.msgprint(_("BOMs creation failed")) + + def create_bom(self, row, production_item_wise_rm): + bom = frappe.new_doc("BOM") + bom.update( + { + "item": row.item_code, + "bom_type": "Production", + "quantity": row.qty, + "allow_alternative_item": 1, + "bom_creator": self.name, + "bom_creator_item": row.name if row.name != self.name else "", + "rm_cost_as_per": "Manual", + } + ) + + for field in BOM_FIELDS: + if self.get(field): + bom.set(field, self.get(field)) + + for item in production_item_wise_rm[(row.item_code, row.name)]["items"]: + bom_no = "" + item.do_not_explode = 1 + if (item.item_code, item.name) in production_item_wise_rm: + bom_no = production_item_wise_rm.get((item.item_code, item.name)).bom_no + item.do_not_explode = 0 + + item_args = {} + for field in BOM_ITEM_FIELDS: + item_args[field] = item.get(field) + + item_args.update( + { + "bom_no": bom_no, + "allow_alternative_item": 1, + "allow_scrap_items": 1, + "include_item_in_manufacturing": 1, + } + ) + + bom.append("items", item_args) + + bom.save(ignore_permissions=True) + bom.submit() + + production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name + + @frappe.whitelist() + def get_default_bom(self, item_code) -> str: + return frappe.get_cached_value("Item", item_code, "default_bom") + + +@frappe.whitelist() +def get_children(doctype=None, parent=None, **kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + fields = [ + "item_code as value", + "is_expandable as expandable", + "parent as parent_id", + "qty", + "idx", + "'BOM Creator Item' as doctype", + "name", + "uom", + "rate", + "amount", + ] + + query_filters = { + "fg_item": parent, + "parent": kwargs.parent_id, + } + + if kwargs.name: + query_filters["name"] = kwargs.name + + return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx") + + +@frappe.whitelist() +def add_item(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + item_info = get_item_details(kwargs.item_code) + kwargs.update( + { + "uom": item_info.stock_uom, + "stock_uom": item_info.stock_uom, + "conversion_factor": 1, + } + ) + + doc.append("items", kwargs) + doc.save() + + return doc + + +@frappe.whitelist() +def add_sub_assembly(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + bom_item = frappe.parse_json(kwargs.bom_item) + + name = kwargs.fg_reference_id + parent_row_no = "" + if not kwargs.convert_to_sub_assembly: + item_info = get_item_details(bom_item.item_code) + item_row = doc.append( + "items", + { + "item_code": bom_item.item_code, + "qty": bom_item.qty, + "uom": item_info.stock_uom, + "fg_item": kwargs.fg_item, + "conversion_factor": 1, + "fg_reference_id": name, + "stock_qty": bom_item.qty, + "fg_reference_id": name, + "do_not_explode": 1, + "is_expandable": 1, + "stock_uom": item_info.stock_uom, + }, + ) + + parent_row_no = item_row.idx + name = "" + + for row in bom_item.get("items"): + row = frappe._dict(row) + item_info = get_item_details(row.item_code) + doc.append( + "items", + { + "item_code": row.item_code, + "qty": row.qty, + "fg_item": bom_item.item_code, + "uom": item_info.stock_uom, + "fg_reference_id": name, + "parent_row_no": parent_row_no, + "conversion_factor": 1, + "do_not_explode": 1, + "stock_qty": row.qty, + "stock_uom": item_info.stock_uom, + }, + ) + + doc.save() + + return doc + + +def get_item_details(item_code): + return frappe.get_cached_value( + "Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1 + ) + + +@frappe.whitelist() +def delete_node(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent) + if kwargs.docname: + frappe.delete_doc("BOM Creator Item", kwargs.docname) + + for item in items: + frappe.delete_doc("BOM Creator Item", item.name) + if item.expandable: + delete_node(fg_item=item.value, parent=item.parent_id) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + doc.set_rate_for_items() + doc.save() + + return doc + + +@frappe.whitelist() +def edit_qty(doctype, docname, qty, parent): + frappe.db.set_value(doctype, docname, "qty", qty) + doc = frappe.get_doc("BOM Creator", parent) + doc.set_rate_for_items() + doc.save() + + return doc diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js new file mode 100644 index 0000000000..423b721e04 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js @@ -0,0 +1,18 @@ +frappe.listview_settings['BOM Creator'] = { + add_fields: ["status"], + get_indicator: function (doc) { + if (doc.status === "Draft") { + return [__("Draft"), "red", "status,=,Draft"]; + } else if (doc.status === "In Progress") { + return [__("In Progress"), "orange", "status,=,In Progress"]; + } else if (doc.status === "Completed") { + return [__("Completed"), "green", "status,=,Completed"]; + } else if (doc.status === "Cancelled") { + return [__("Cancelled"), "red", "status,=,Cancelled"]; + } else if (doc.status === "Failed") { + return [__("Failed"), "red", "status,=,Failed"]; + } else if (doc.status === "Submitted") { + return [__("Submitted"), "blue", "status,=,Submitted"]; + } + }, +}; diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py new file mode 100644 index 0000000000..d239d58131 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -0,0 +1,240 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import random + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.manufacturing.doctype.bom_creator.bom_creator import ( + add_item, + add_sub_assembly, + delete_node, + edit_qty, +) +from erpnext.stock.doctype.item.test_item import make_item + + +class TestBOMCreator(FrappeTestCase): + def setUp(self) -> None: + create_items() + + def test_bom_sub_assembly(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM with Sub Assembly", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_sub_assembly( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + bom_item={ + "item_code": "Frame Assembly", + "qty": 1, + "items": [ + { + "item_code": "Frame", + "qty": 1, + }, + { + "item_code": "Fork", + "qty": 1, + }, + ], + }, + ) + + doc.reload() + self.assertEqual(doc.items[0].item_code, "Frame Assembly") + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Frame Assembly") + self.assertEqual(row.fg_reference_id, doc.items[0].name) + + self.assertEqual(doc.items[0].amount, fg_valuation_rate) + + def test_bom_raw_material(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM with Raw Material", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_item( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + item_code="Pedal Assembly", + qty=2, + ) + + doc.reload() + self.assertEqual(doc.items[0].item_code, "Pedal Assembly") + self.assertEqual(doc.items[0].qty, 2) + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Bicycle") + self.assertEqual(row.fg_reference_id, doc.name) + + self.assertEqual(doc.raw_material_cost, fg_valuation_rate) + + def test_convert_to_sub_assembly(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_item( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + item_code="Pedal Assembly", + qty=2, + ) + + doc.reload() + self.assertEqual(doc.items[0].is_expandable, 0) + + add_sub_assembly( + convert_to_sub_assembly=1, + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.items[0].name, + bom_item={ + "item_code": "Pedal Assembly", + "qty": 2, + "items": [ + { + "item_code": "Pedal Body", + "qty": 2, + }, + { + "item_code": "Pedal Axle", + "qty": 2, + }, + ], + }, + ) + + doc.reload() + self.assertEqual(doc.items[0].is_expandable, 1) + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Pedal Assembly") + self.assertEqual(row.qty, 2.0) + self.assertEqual(row.fg_reference_id, doc.items[0].name) + + self.assertEqual(doc.raw_material_cost, fg_valuation_rate) + + +def create_items(): + raw_materials = [ + "Frame", + "Fork", + "Rim", + "Spokes", + "Hub", + "Tube", + "Tire", + "Pedal Body", + "Pedal Axle", + "Ball Bearings", + "Chain Links", + "Chain Pins", + "Seat", + "Seat Post", + "Seat Clamp", + ] + + for item in raw_materials: + valuation_rate = random.choice([100, 200, 300, 500, 333, 222, 44, 20, 10]) + make_item( + item, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + "valuation_rate": valuation_rate, + }, + ) + + sub_assemblies = [ + "Frame Assembly", + "Wheel Assembly", + "Pedal Assembly", + "Chain Assembly", + "Seat Assembly", + ] + + for item in sub_assemblies: + make_item( + item, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + +def make_bom_creator(**kwargs): + if isinstance(kwargs, str) or isinstance(kwargs, dict): + kwargs = frappe.parse_json(kwargs) + + doc = frappe.new_doc("BOM Creator") + doc.update(kwargs) + doc.save() + + return doc diff --git a/erpnext/manufacturing/doctype/bom_creator_item/__init__.py b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json new file mode 100644 index 0000000000..fdb5d3ad33 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -0,0 +1,243 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-07-18 14:35:50.307386", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "item_group", + "column_break_f63f", + "fg_item", + "source_warehouse", + "is_expandable", + "sourced_by_supplier", + "bom_created", + "description_section", + "description", + "quantity_and_rate_section", + "qty", + "rate", + "uom", + "column_break_bgnb", + "stock_qty", + "conversion_factor", + "stock_uom", + "amount_section", + "amount", + "column_break_yuca", + "base_rate", + "base_amount", + "section_break_wtld", + "do_not_explode", + "parent_row_no", + "fg_reference_id", + "column_break_sulm", + "instruction" + ], + "fields": [ + { + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "column_break_f63f", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "fg_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "FG Item", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "source_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "is_expandable", + "fieldtype": "Check", + "label": "Is Expandable", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, + "fieldname": "description", + "fieldtype": "Small Text" + }, + { + "fieldname": "quantity_and_rate_section", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, + { + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty" + }, + { + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate" + }, + { + "columns": 1, + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_bgnb", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "label": "Stock Qty", + "read_only": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "no_copy": 1, + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "amount_section", + "fieldtype": "Section Break", + "label": "Amount" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 + }, + { + "fieldname": "column_break_yuca", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "do_not_explode", + "fieldtype": "Check", + "hidden": 1, + "label": "Do Not Explode" + }, + { + "fieldname": "instruction", + "fieldtype": "Small Text", + "label": "Instruction" + }, + { + "fieldname": "base_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Amount" + }, + { + "fieldname": "base_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Rate" + }, + { + "default": "0", + "fieldname": "sourced_by_supplier", + "fieldtype": "Check", + "label": "Sourced by Supplier" + }, + { + "fieldname": "section_break_wtld", + "fieldtype": "Section Break" + }, + { + "fieldname": "fg_reference_id", + "fieldtype": "Data", + "label": "FG Reference", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_sulm", + "fieldtype": "Column Break" + }, + { + "fieldname": "parent_row_no", + "fieldtype": "Data", + "label": "Parent Row No", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "bom_created", + "fieldtype": "Check", + "hidden": 1, + "label": "BOM Created", + "no_copy": 1, + "print_hide": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-07 11:52:30.492233", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Creator Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py new file mode 100644 index 0000000000..350c9180b9 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BOMCreatorItem(Document): + pass diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 518ae14659..8e0785074f 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "custom_blocks": [], "docstatus": 0, @@ -316,7 +316,7 @@ "type": "Link" } ], - "modified": "2023-07-04 14:40:47.281125", + "modified": "2023-08-08 22:28:39.633891", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,6 +336,13 @@ "type": "URL", "url": "https://frappe.school/courses/manufacturing?utm_source=in_app" }, + { + "color": "Grey", + "doc_view": "List", + "label": "BOM Creator", + "link_to": "BOM Creator", + "type": "DocType" + }, { "color": "Grey", "doc_view": "List", diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js new file mode 100644 index 0000000000..b3b2e9f9b8 --- /dev/null +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -0,0 +1,416 @@ +class BOMConfigurator { + constructor({ wrapper, page, frm, bom_configurator }) { + this.$wrapper = $(wrapper); + this.page = page; + this.bom_configurator = bom_configurator; + this.frm = frm; + + this.make(); + this.prepare_layout(); + this.bind_events(); + } + + add_boms() { + this.frm.call({ + method: "add_boms", + freeze: true, + doc: this.frm.doc, + }); + } + + make() { + let options = { + ...this.tree_options(), + ...this.tree_methods(), + }; + + frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options); + this.tree_view = frappe.views.trees["BOM Configurator"]; + } + + bind_events() { + frappe.views.trees["BOM Configurator"].events = { + frm: this.frm, + add_item: this.add_item, + add_sub_assembly: this.add_sub_assembly, + get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields, + convert_to_sub_assembly: this.convert_to_sub_assembly, + delete_node: this.delete_node, + edit_qty: this.edit_qty, + load_tree: this.load_tree, + set_default_qty: this.set_default_qty, + } + } + + tree_options() { + return { + parent: this.$wrapper.get(0), + body: this.$wrapper.get(0), + doctype: 'BOM Configurator', + page: this.page, + expandable: true, + title: __("Configure Product Assembly"), + breadcrumb: "Manufacturing", + get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children", + root_label: this.frm.doc.item_code, + disable_add_node: true, + get_tree_root: false, + show_expand_all: false, + extend_toolbar: false, + do_not_make_page: true, + do_not_setup_menu: true, + } + } + + tree_methods() { + let frm_obj = this; + let view = frappe.views.trees["BOM Configurator"]; + + return { + onload: function(me) { + me.args["parent_id"] = frm_obj.frm.doc.name; + me.args["parent"] = frm_obj.frm.doc.item_code; + me.parent = frm_obj.$wrapper.get(0); + me.body = frm_obj.$wrapper.get(0); + me.make_tree(); + }, + onrender(node) { + const qty = node.data.qty || frm_obj.frm.doc.qty; + const uom = node.data.uom || frm_obj.frm.doc.uom; + const docname = node.data.name || frm_obj.frm.doc.name; + let amount = node.data.amount; + if (node.data.value === frm_obj.frm.doc.item_code) { + amount = frm_obj.frm.doc.raw_material_cost; + } + + amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency }); + + $(` +
+
${qty} ${uom}
+
+ ${amount} +
+
+ + `).insertBefore(node.$ul); + }, + toolbar: this.frm?.doc.docstatus === 0 ? [ + { + label:__(frappe.utils.icon('edit', 'sm') + " Qty"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.edit_qty(node, view); + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('add', 'sm') + " Raw Material"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.add_item(node, view); + }, + condition: function(node) { + return node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('add', 'sm') + " Sub Assembly"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.add_sub_assembly(node, view); + }, + condition: function(node) { + return node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__("Expand All"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + + if (!node.expanded) { + view.tree.load_children(node, true); + $(node.parent[0]).find(".tree-children").show(); + node.$toolbar.find(".expand-all-btn").html("Collapse All"); + } else { + node.$tree_link.trigger("click"); + node.$toolbar.find(".expand-all-btn").html("Expand All"); + } + }, + condition: function(node) { + return node.expandable && node.is_root; + }, + btnClass: "hidden-xs expand-all-btn" + }, + { + label:__(frappe.utils.icon('move', 'sm') + " Sub Assembly"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.convert_to_sub_assembly(node, view); + }, + condition: function(node) { + return !node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('delete', 'sm') + __(" Item")), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.delete_node(node, view); + }, + condition: function(node) { + return !node.is_root; + }, + btnClass: "hidden-xs" + }, + ] : [{ + label:__("Expand All"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + + if (!node.expanded) { + view.tree.load_children(node, true); + $(node.parent[0]).find(".tree-children").show(); + node.$toolbar.find(".expand-all-btn").html("Collapse All"); + } else { + node.$tree_link.trigger("click"); + node.$toolbar.find(".expand-all-btn").html("Expand All"); + } + }, + condition: function(node) { + return node.expandable && node.is_root; + }, + btnClass: "hidden-xs expand-all-btn" + }], + } + } + + add_item(node, view) { + frappe.prompt([ + { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 }, + ], + (data) => { + if (!node.data.parent_id) { + node.data.parent_id = this.frm.doc.name; + } + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + item_code: data.item_code, + fg_reference_id: node.data.name || this.frm.doc.name, + qty: data.qty, + }, + callback: (r) => { + view.events.load_tree(r, node); + } + }); + }, + __("Add Item"), + __("Add")); + } + + add_sub_assembly(node, view) { + let dialog = new frappe.ui.Dialog({ + fields: view.events.get_sub_assembly_modal_fields(), + title: __("Add Sub Assembly"), + }); + + dialog.show(); + view.events.set_default_qty(dialog); + + dialog.set_primary_action(__("Add"), () => { + let bom_item = dialog.get_values(); + + if (!node.data?.parent_id) { + node.data.parent_id = this.frm.doc.name; + } + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + fg_reference_id: node.data.name || this.frm.doc.name, + bom_item: bom_item, + }, + callback: (r) => { + view.events.load_tree(r, node); + } + }); + + dialog.hide(); + }); + + } + + get_sub_assembly_modal_fields(read_only=false) { + return [ + { label: __("Sub Assembly Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, read_only: read_only }, + { fieldtype: "Column Break" }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, read_only: read_only }, + { fieldtype: "Section Break" }, + { label: __("Raw Materials"), fieldname: "items", fieldtype: "Table", reqd: 1, + fields: [ + { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, in_list_view: 1 }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, in_list_view: 1 }, + ] + }, + ] + } + + convert_to_sub_assembly(node, view) { + let dialog = new frappe.ui.Dialog({ + fields: view.events.get_sub_assembly_modal_fields(true), + title: __("Add Sub Assembly"), + }); + + dialog.set_values({ + item_code: node.data.value, + qty: node.data.qty, + }); + + dialog.show(); + view.events.set_default_qty(dialog); + + dialog.set_primary_action(__("Add"), () => { + let bom_item = dialog.get_values(); + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + bom_item: bom_item, + fg_reference_id: node.data.name || this.frm.doc.name, + convert_to_sub_assembly: true, + }, + callback: (r) => { + node.expandable = true; + view.events.load_tree(r, node); + } + }); + + dialog.hide(); + }); + } + + set_default_qty(dialog) { + dialog.fields_dict.items.grid.fields_map.item_code.onchange = function (event) { + if (event) { + let name = $(event.currentTarget).closest('.grid-row').attr("data-name") + let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc; + item_row.qty = 1; + dialog.fields_dict.items.grid.refresh() + } + } + } + + delete_node(node, view) { + frappe.confirm(__("Are you sure you want to delete this Item?"), () => { + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + doctype: node.data.doctype, + docname: node.data.name, + }, + callback: (r) => { + view.events.load_tree(r, node.parent_node); + } + }); + }); + } + + edit_qty(node, view) { + let qty = node.data.qty || this.frm.doc.qty; + frappe.prompt([ + { label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }, + ], + (data) => { + let doctype = node.data.doctype || this.frm.doc.doctype; + let docname = node.data.name || this.frm.doc.name; + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty", + args: { + doctype: doctype, + docname: docname, + qty: data.qty, + parent: node.data.parent_id, + }, + callback: (r) => { + node.data.qty = data.qty; + let uom = node.data.uom || this.frm.doc.uom; + $(node.parent.get(0)).find(`[data-bom-qty-docname='${docname}']`).html(data.qty + " " + uom); + view.events.load_tree(r, node); + } + }); + }, + __("Edit Qty"), + __("Update")); + } + + prepare_layout() { + let main_div = $(this.page)[0]; + + main_div.style.marginBottom = "15px"; + $(main_div).find(".tree-children")[0].style.minHeight = "370px"; + $(main_div).find(".tree-children")[0].style.maxHeight = "370px"; + $(main_div).find(".tree-children")[0].style.overflowY = "auto"; + } + + load_tree(response, node) { + let item_row = ""; + let parent_dom = "" + let total_amount = response.message.raw_material_cost; + + frappe.views.trees["BOM Configurator"].tree.load_children(node); + + while (true) { + item_row = response.message.items.filter(item => item.name === node.data.name); + + if (item_row?.length) { + node.data.amount = item_row[0].amount; + total_amount = node.data.amount + } else { + total_amount = response.message.raw_material_cost; + } + + parent_dom = $(node.parent.get(0)); + total_amount = frappe.format( + total_amount, { + fieldtype: "Currency", + currency: this.frm.doc.currency + } + ); + + $($(parent_dom).find(".fg-item-amt")[0]).html(total_amount); + + if (node.is_root) { + break; + } + + node = node.parent_node; + } + + } +} + +frappe.ui.BOMConfigurator = BOMConfigurator; \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index c11d123982..a3c10c68a7 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -114,6 +114,10 @@ $.extend(erpnext.utils, { }, view_serial_batch_nos: function(frm) { + if (!frm.doc?.items) { + return; + } + let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle); if (bundle_ids?.length) { From 6349b67df497623ce3f56da3d9f3c94295c3dd3b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 17:54:14 +0530 Subject: [PATCH 16/60] fix(demo): Default accounts for demo company --- .../verified/id_chart_of_accounts.json | 3 +-- erpnext/public/js/setup_wizard.js | 3 ++- erpnext/setup/doctype/company/company.py | 16 ++++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json index d1a0defba9..12f517d64b 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json @@ -69,8 +69,7 @@ "Persediaan Barang": { "Persediaan Barang": { "account_number": "1141.000", - "account_type": "Stock", - "is_group": 1 + "account_type": "Stock" }, "Uang Muka Pembelian": { "Uang Muka Pembelian": { diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index ba200ef168..3cbec3e8ef 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -45,7 +45,8 @@ erpnext.setup.slides_settings = [ fieldname: 'setup_demo', label: __('Generate Demo Data for Exploration'), fieldtype: 'Check', - description: 'If checked, we will create demo data for you to explore the system. This demo data can be erased later.'}, + description: __('If checked, we will create demo data for you to explore the system. This demo data can be erased later.') + }, ], onload: function (slide) { diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index fcdf245659..2db27528cc 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -403,14 +403,18 @@ class Company(NestedSet): self._set_default_account(default_account, default_accounts.get(default_account)) if not self.default_income_account: - income_account = frappe.db.get_value( - "Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0} + income_account = frappe.db.get_all( + "Account", + filters={"company": self.name, "is_group": 0}, + or_filters={ + "account_name": ("in", [_("Sales"), _("Sales Account")]), + "account_type": "Income Account", + }, + pluck="name", ) - if not income_account: - income_account = frappe.db.get_value( - "Account", {"account_name": _("Sales Account"), "company": self.name} - ) + if income_account: + income_account = income_account[0] self.db_set("default_income_account", income_account) From 5f75aea6fab48675f4606d5d9b36425d2d7cbbc4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 17:58:51 +0530 Subject: [PATCH 17/60] fix(demo): Default accounts for demo company --- .../verified/ae_uae_chart_template_standard.json | 16 ++++++++++++---- .../verified/id_chart_of_accounts.json | 3 ++- erpnext/accounts/general_ledger.py | 4 ++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json index a8afb55df6..3a3b6e399e 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json @@ -437,12 +437,20 @@ }, "Sales": { "Sales from Other Regions": { - "Sales from Other Region": {} + "Sales from Other Region": { + "account_type": "Income Account" + } }, "Sales of same region": { - "Management Consultancy Fees 1": {}, - "Sales Account": {}, - "Sales of I/C": {} + "Management Consultancy Fees 1": { + "account_type": "Income Account" + }, + "Sales Account": { + "account_type": "Income Account" + }, + "Sales of I/C": { + "account_type": "Income Account" + } } }, "root_type": "Income" diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json index 12f517d64b..fb974765db 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json @@ -669,7 +669,8 @@ }, "Penjualan Barang Dagangan": { "Penjualan": { - "account_number": "4110.000" + "account_number": "4110.000", + "account_type": "Income Account" }, "Potongan Penjualan": { "account_number": "4130.000" diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 3803836ef7..d4967785ba 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -539,6 +539,10 @@ def get_round_off_account_and_cost_center( "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] + # Use expense account as fallback + if not round_off_account: + round_off_account = frappe.get_cached_value("Company", company, "default_expense_account") + meta = frappe.get_meta(voucher_type) # Give first preference to parent cost center for round off GLE From 299e32befd50d32129ca58cba386660cd0179dfb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 18:02:06 +0530 Subject: [PATCH 18/60] chore: Linting Issues --- erpnext/accounts/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d6d0f91fb..1aefeaacf7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,9 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering - limit=None, # passed by reconciliation tool - voucher_no=None, # filter passed by reconciliation tool + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") From 0120588f5fa6cc3cb7791dac8cb86c788dc21f19 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 18:32:16 +0530 Subject: [PATCH 19/60] chore: Handle edge cases --- erpnext/setup/doctype/company/company.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 2db27528cc..b05696ad96 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -415,6 +415,8 @@ class Company(NestedSet): if income_account: income_account = income_account[0] + else: + income_account = None self.db_set("default_income_account", income_account) From f78b6d00817e9f73ffb343e1b2363db510639466 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 18:56:16 +0530 Subject: [PATCH 20/60] refactor: code cleanup --- .../subcontracting_receipt.py | 315 ++++++++++-------- 1 file changed, 175 insertions(+), 140 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index b1a585d147..2c7b2eb99e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -36,33 +36,6 @@ class SubcontractingReceipt(SubcontractingController): ), ) - def update_status_updater_args(self): - if cint(self.is_return): - self.status_updater.extend( - [ - { - "source_dt": "Subcontracting Receipt Item", - "target_dt": "Subcontracting Order Item", - "join_field": "subcontracting_order_item", - "target_field": "returned_qty", - "source_field": "-1 * qty", - "extra_cond": """ and exists (select name from `tabSubcontracting Receipt` - where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""", - }, - { - "source_dt": "Subcontracting Receipt Item", - "target_dt": "Subcontracting Receipt Item", - "join_field": "subcontracting_receipt_item", - "target_field": "returned_qty", - "target_parent_dt": "Subcontracting Receipt", - "target_parent_field": "per_returned", - "target_ref_field": "received_qty", - "source_field": "-1 * received_qty", - "percent_join_field_parent": "return_against", - }, - ] - ) - def before_validate(self): super(SubcontractingReceipt, self).before_validate() self.validate_items_qty() @@ -71,16 +44,18 @@ class SubcontractingReceipt(SubcontractingController): self.set_items_expense_account() def validate(self): - if ( - frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") - == "BOM" - ): - self.supplied_items = [] + self.reset_supplied_items() super(SubcontractingReceipt, self).validate() + + if self.is_new() and self.get("_action") == "save": + self.get_scrap_items() + self.set_missing_values() self.validate_posting_time() - self.validate_accepted_warehouse() - self.validate_rejected_warehouse() + + if self.get("_action") == "submit": + self.validate_accepted_warehouse() + self.validate_rejected_warehouse() if getdate(self.posting_date) > getdate(nowdate()): frappe.throw(_("Posting Date cannot be future date")) @@ -89,11 +64,6 @@ class SubcontractingReceipt(SubcontractingController): self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.get_current_stock() - def on_update(self): - for table_field in ["items", "supplied_items"]: - if self.get(table_field): - self.set_serial_and_batch_bundle(table_field) - def on_submit(self): self.validate_available_qty_for_consumption() self.update_status_updater_args() @@ -105,6 +75,11 @@ class SubcontractingReceipt(SubcontractingController): self.repost_future_sle_and_gle() self.update_status() + def on_update(self): + for table_field in ["items", "supplied_items"]: + if self.get(table_field): + self.set_serial_and_batch_bundle(table_field) + def on_cancel(self): self.ignore_linked_doctypes = ( "GL Entry", @@ -122,107 +97,6 @@ class SubcontractingReceipt(SubcontractingController): self.set_subcontracting_order_status() self.update_status() - @frappe.whitelist() - def set_missing_values(self): - self.calculate_additional_costs() - self.calculate_supplied_items_qty_and_amount() - self.calculate_items_qty_and_amount() - - def validate_accepted_warehouse(self): - for item in self.get("items"): - if flt(item.qty) and not item.warehouse: - if self.set_warehouse: - item.warehouse = self.set_warehouse - else: - frappe.throw( - _("Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}").format( - item.idx, item.item_code - ) - ) - - if item.get("warehouse") and (item.get("warehouse") == item.get("rejected_warehouse")): - frappe.throw( - _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) - ) - - def set_available_qty_for_consumption(self): - supplied_items_details = {} - - sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") - for item in self.get("items"): - supplied_items = ( - frappe.qb.from_(sco_supplied_item) - .select( - sco_supplied_item.rm_item_code, - sco_supplied_item.reference_name, - (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), - ) - .where( - (sco_supplied_item.parent == item.subcontracting_order) - & (sco_supplied_item.main_item_code == item.item_code) - & (sco_supplied_item.reference_name == item.subcontracting_order_item) - ) - ).run(as_dict=True) - - if supplied_items: - supplied_items_details[item.name] = {} - - for supplied_item in supplied_items: - supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty - else: - for item in self.get("supplied_items"): - item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( - item.rm_item_code, 0 - ) - - def calculate_supplied_items_qty_and_amount(self): - for item in self.get("supplied_items") or []: - item.amount = item.rate * item.consumed_qty - - self.set_available_qty_for_consumption() - - def calculate_items_qty_and_amount(self): - rm_supp_cost = {} - for item in self.get("supplied_items") or []: - if item.reference_name in rm_supp_cost: - rm_supp_cost[item.reference_name] += item.amount - else: - rm_supp_cost[item.reference_name] = item.amount - - total_qty = total_amount = 0 - for item in self.items: - if item.qty and item.name in rm_supp_cost: - item.rm_supp_cost = rm_supp_cost[item.name] - item.rm_cost_per_qty = item.rm_supp_cost / item.qty - rm_supp_cost.pop(item.name) - - if item.recalculate_rate: - item.rate = ( - flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) - ) - - item.received_qty = flt(item.qty) + flt(item.rejected_qty) - item.amount = flt(item.qty) * flt(item.rate) - total_qty += flt(item.qty) - total_amount += item.amount - else: - self.total_qty = total_qty - self.total = total_amount - - def validate_available_qty_for_consumption(self): - for item in self.get("supplied_items"): - precision = item.precision("consumed_qty") - if ( - item.available_qty_for_consumption - and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 - ): - msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} - must be less than or equal to Available Qty For Consumption - {flt(item.available_qty_for_consumption, precision)} - in Consumed Items Table.""" - - frappe.throw(_(msg)) - def validate_items_qty(self): for item in self.items: if not (item.qty or item.rejected_qty): @@ -264,6 +138,167 @@ class SubcontractingReceipt(SubcontractingController): if not item.expense_account: item.expense_account = expense_account + def reset_supplied_items(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "BOM" + ): + self.supplied_items = [] + + def get_scrap_items(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "BOM" + ): + for item in list(self.items): + if item.bom and not item.is_scrap_item: + bom = frappe.get_doc("BOM", item.bom) + for scrap_item in bom.scrap_items: + qty = flt(item.received_qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) + self.append( + "items", + { + "item_code": scrap_item.item_code, + "item_name": scrap_item.item_name, + "qty": qty, + "stock_uom": scrap_item.stock_uom, + "rate": scrap_item.rate, + "amount": qty * scrap_item.rate, + "is_scrap_item": 1, + "warehouse": self.set_warehouse, + "rejected_warehouse": self.rejected_warehouse, + "service_cost_per_qty": 0, + }, + ) + + @frappe.whitelist() + def set_missing_values(self): + self.calculate_additional_costs() + self.calculate_supplied_items_qty_and_amount() + self.calculate_items_qty_and_amount() + + def calculate_supplied_items_qty_and_amount(self): + for item in self.get("supplied_items") or []: + item.amount = item.rate * item.consumed_qty + + self.set_available_qty_for_consumption() + + def set_available_qty_for_consumption(self): + supplied_items_details = {} + + sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") + for item in self.get("items"): + supplied_items = ( + frappe.qb.from_(sco_supplied_item) + .select( + sco_supplied_item.rm_item_code, + sco_supplied_item.reference_name, + (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), + ) + .where( + (sco_supplied_item.parent == item.subcontracting_order) + & (sco_supplied_item.main_item_code == item.item_code) + & (sco_supplied_item.reference_name == item.subcontracting_order_item) + ) + ).run(as_dict=True) + + if supplied_items: + supplied_items_details[item.name] = {} + + for supplied_item in supplied_items: + supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty + else: + for item in self.get("supplied_items"): + item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( + item.rm_item_code, 0 + ) + + def calculate_items_qty_and_amount(self): + rm_supp_cost = {} + for item in self.get("supplied_items") or []: + if item.reference_name in rm_supp_cost: + rm_supp_cost[item.reference_name] += item.amount + else: + rm_supp_cost[item.reference_name] = item.amount + + total_qty = total_amount = 0 + for item in self.items: + if item.qty and item.name in rm_supp_cost: + item.rm_supp_cost = rm_supp_cost[item.name] + item.rm_cost_per_qty = item.rm_supp_cost / item.qty + rm_supp_cost.pop(item.name) + + if item.recalculate_rate: + item.rate = ( + flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) + ) + + item.received_qty = flt(item.qty) + flt(item.rejected_qty) + item.amount = flt(item.qty) * flt(item.rate) + total_qty += flt(item.qty) + total_amount += item.amount + else: + self.total_qty = total_qty + self.total = total_amount + + def validate_accepted_warehouse(self): + for item in self.get("items"): + if flt(item.qty) and not item.warehouse: + if self.set_warehouse: + item.warehouse = self.set_warehouse + else: + frappe.throw( + _("Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}").format( + item.idx, item.item_code + ) + ) + + if item.get("warehouse") and (item.get("warehouse") == item.get("rejected_warehouse")): + frappe.throw( + _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx) + ) + + def validate_available_qty_for_consumption(self): + for item in self.get("supplied_items"): + precision = item.precision("consumed_qty") + if ( + item.available_qty_for_consumption + and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 + ): + msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} + must be less than or equal to Available Qty For Consumption + {flt(item.available_qty_for_consumption, precision)} + in Consumed Items Table.""" + + frappe.throw(_(msg)) + + def update_status_updater_args(self): + if cint(self.is_return): + self.status_updater.extend( + [ + { + "source_dt": "Subcontracting Receipt Item", + "target_dt": "Subcontracting Order Item", + "join_field": "subcontracting_order_item", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "extra_cond": """ and exists (select name from `tabSubcontracting Receipt` + where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""", + }, + { + "source_dt": "Subcontracting Receipt Item", + "target_dt": "Subcontracting Receipt Item", + "join_field": "subcontracting_receipt_item", + "target_field": "returned_qty", + "target_parent_dt": "Subcontracting Receipt", + "target_parent_field": "per_returned", + "target_ref_field": "received_qty", + "source_field": "-1 * received_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) + def update_status(self, status=None, update_modified=False): if not status: if self.docstatus == 0: From 794edbb334660287d0264aa3a3c4aaa1d1033df7 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 22:02:29 +0530 Subject: [PATCH 21/60] feat: add `Scrap Cost Per Qty` field in SCR Item --- .../subcontracting_receipt_item.json | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index b6f1155ce3..1ac5bcb480 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -33,6 +33,7 @@ "rm_cost_per_qty", "service_cost_per_qty", "additional_cost_per_qty", + "scrap_cost_per_qty", "rm_supp_cost", "warehouse_and_reference", "warehouse", @@ -44,6 +45,7 @@ "bom", "quality_inspection", "schedule_date", + "reference_name", "section_break_45", "serial_and_batch_bundle", "serial_no", @@ -158,6 +160,7 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", + "read_only_depends_on": "eval: doc.is_scrap_item", "width": "100px" }, { @@ -215,6 +218,8 @@ "fieldtype": "Column Break" }, { + "default": "0", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "rm_cost_per_qty", "fieldtype": "Currency", "label": "Raw Material Cost Per Qty", @@ -222,6 +227,8 @@ "read_only": 1 }, { + "default": "0", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "service_cost_per_qty", "fieldtype": "Currency", "label": "Service Cost Per Qty", @@ -261,6 +268,7 @@ "options": "Warehouse", "print_hide": 1, "print_width": "100px", + "read_only_depends_on": "eval: doc.is_scrap_item", "width": "100px" }, { @@ -452,6 +460,7 @@ "print_hide": 1 }, { + "default": "0", "depends_on": "returned_qty", "fieldname": "returned_qty", "fieldtype": "Float", @@ -496,12 +505,29 @@ "no_copy": 1, "print_hide": 1, "read_only_depends_on": "eval: doc.bom" + }, + { + "default": "0", + "depends_on": "eval: !doc.is_scrap_item", + "fieldname": "scrap_cost_per_qty", + "fieldtype": "Float", + "label": "Scrap Cost Per Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "label": "Reference Name", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-08-24 12:14:25.123857", + "modified": "2023-08-24 21:57:15.897010", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From a6b2cf3acdb9e83ca41bc01b0796d531faf59d1b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 22:07:24 +0530 Subject: [PATCH 22/60] fix: reduce scrap cost from FG total cost --- .../subcontracting_receipt.py | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 2c7b2eb99e..07f70d1188 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -150,11 +150,13 @@ class SubcontractingReceipt(SubcontractingController): frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") == "BOM" ): + self.remove_scrap_items() + for item in list(self.items): - if item.bom and not item.is_scrap_item: + if item.bom: bom = frappe.get_doc("BOM", item.bom) for scrap_item in bom.scrap_items: - qty = flt(item.received_qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) + qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) self.append( "items", { @@ -168,9 +170,15 @@ class SubcontractingReceipt(SubcontractingController): "warehouse": self.set_warehouse, "rejected_warehouse": self.rejected_warehouse, "service_cost_per_qty": 0, + "reference_name": item.name, }, ) + def remove_scrap_items(self): + for item in list(self.items): + if item.is_scrap_item: + self.remove(item) + @frappe.whitelist() def set_missing_values(self): self.calculate_additional_costs() @@ -214,27 +222,44 @@ class SubcontractingReceipt(SubcontractingController): ) def calculate_items_qty_and_amount(self): - rm_supp_cost = {} + rm_cost_map = {} for item in self.get("supplied_items") or []: - if item.reference_name in rm_supp_cost: - rm_supp_cost[item.reference_name] += item.amount + if item.reference_name in rm_cost_map: + rm_cost_map[item.reference_name] += item.amount else: - rm_supp_cost[item.reference_name] = item.amount + rm_cost_map[item.reference_name] = item.amount + + scrap_cost_map = {} + for item in self.get("items") or []: + if item.is_scrap_item: + if item.reference_name in scrap_cost_map: + scrap_cost_map[item.reference_name] += item.amount + else: + scrap_cost_map[item.reference_name] = item.amount total_qty = total_amount = 0 for item in self.items: - if item.qty and item.name in rm_supp_cost: - item.rm_supp_cost = rm_supp_cost[item.name] - item.rm_cost_per_qty = item.rm_supp_cost / item.qty - rm_supp_cost.pop(item.name) + if item.qty and not item.is_scrap_item: + if item.name in rm_cost_map: + item.rm_supp_cost = rm_cost_map[item.name] + item.rm_cost_per_qty = item.rm_supp_cost / item.qty + rm_cost_map.pop(item.name) + + if item.name in scrap_cost_map: + item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty + scrap_cost_map.pop(item.name) if item.recalculate_rate: item.rate = ( - flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) + flt(item.rm_cost_per_qty) + + flt(item.service_cost_per_qty) + + flt(item.additional_cost_per_qty) + - flt(item.scrap_cost_per_qty) ) item.received_qty = flt(item.qty) + flt(item.rejected_qty) item.amount = flt(item.qty) * flt(item.rate) + total_qty += flt(item.qty) total_amount += item.amount else: From 40a6b5cefef6abba086f06fff78e919f5f779bbc Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 25 Aug 2023 09:35:29 +0530 Subject: [PATCH 23/60] fix: don't recalculate rate for scrap items --- .../subcontracting_receipt.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 07f70d1188..9bf21030b4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -171,6 +171,7 @@ class SubcontractingReceipt(SubcontractingController): "rejected_warehouse": self.rejected_warehouse, "service_cost_per_qty": 0, "reference_name": item.name, + "recalculate_rate": 0, }, ) @@ -239,23 +240,24 @@ class SubcontractingReceipt(SubcontractingController): total_qty = total_amount = 0 for item in self.items: - if item.qty and not item.is_scrap_item: - if item.name in rm_cost_map: - item.rm_supp_cost = rm_cost_map[item.name] - item.rm_cost_per_qty = item.rm_supp_cost / item.qty - rm_cost_map.pop(item.name) + if not item.is_scrap_item: + if item.qty: + if item.name in rm_cost_map: + item.rm_supp_cost = rm_cost_map[item.name] + item.rm_cost_per_qty = item.rm_supp_cost / item.qty + rm_cost_map.pop(item.name) - if item.name in scrap_cost_map: - item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty - scrap_cost_map.pop(item.name) + if item.name in scrap_cost_map: + item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty + scrap_cost_map.pop(item.name) - if item.recalculate_rate: - item.rate = ( - flt(item.rm_cost_per_qty) - + flt(item.service_cost_per_qty) - + flt(item.additional_cost_per_qty) - - flt(item.scrap_cost_per_qty) - ) + if item.recalculate_rate: + item.rate = ( + flt(item.rm_cost_per_qty) + + flt(item.service_cost_per_qty) + + flt(item.additional_cost_per_qty) + - flt(item.scrap_cost_per_qty) + ) item.received_qty = flt(item.qty) + flt(item.rejected_qty) item.amount = flt(item.qty) * flt(item.rate) From 199071b7731d729ddb1248dace1ecda6bd6743bb Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 25 Aug 2023 09:36:12 +0530 Subject: [PATCH 24/60] fix(ux): make `Recalculate Rate` field hidden for scrap items --- .../subcontracting_receipt_item.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 1ac5bcb480..5442dfddd7 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -476,9 +476,11 @@ }, { "default": "1", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "recalculate_rate", "fieldtype": "Check", - "label": "Recalculate Rate" + "label": "Recalculate Rate", + "read_only_depends_on": "eval: doc.is_scrap_item" }, { "fieldname": "serial_and_batch_bundle", @@ -519,6 +521,7 @@ { "fieldname": "reference_name", "fieldtype": "Data", + "hidden": 1, "label": "Reference Name", "no_copy": 1, "read_only": 1 @@ -527,7 +530,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-08-24 21:57:15.897010", + "modified": "2023-08-25 09:33:47.232140", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From 7a6db924d524ebaec9101c64087db69d06a232ef Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 25 Aug 2023 09:56:48 +0530 Subject: [PATCH 25/60] fix: validate scrap items --- .../subcontracting_receipt.py | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 9bf21030b4..36be4d1035 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -45,21 +45,23 @@ class SubcontractingReceipt(SubcontractingController): def validate(self): self.reset_supplied_items() + self.validate_posting_time() + + if getdate(self.posting_date) > getdate(nowdate()): + frappe.throw(_("Posting Date cannot be future date")) + super(SubcontractingReceipt, self).validate() if self.is_new() and self.get("_action") == "save": self.get_scrap_items() self.set_missing_values() - self.validate_posting_time() if self.get("_action") == "submit": + self.validate_scrap_items() self.validate_accepted_warehouse() self.validate_rejected_warehouse() - if getdate(self.posting_date) > getdate(nowdate()): - frappe.throw(_("Posting Date cannot be future date")) - self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.get_current_stock() @@ -268,6 +270,28 @@ class SubcontractingReceipt(SubcontractingController): self.total_qty = total_qty self.total = total_amount + def validate_scrap_items(self): + for item in self.items: + if item.is_scrap_item: + if not item.qty: + frappe.throw( + _("Row #{0}: Scrap Item Qty cannot be zero").format(item.idx), + ) + + if item.rejected_qty: + frappe.throw( + _("Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ), + ) + + if not item.reference_name: + frappe.throw( + _("Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ), + ) + def validate_accepted_warehouse(self): for item in self.get("items"): if flt(item.qty) and not item.warehouse: From 9b47617117988a73a5f9c3bc36693d14809e896f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 25 Aug 2023 10:53:13 +0530 Subject: [PATCH 26/60] feat: button to get Scrap Items --- .../subcontracting_receipt/subcontracting_receipt.js | 4 ++++ .../subcontracting_receipt/subcontracting_receipt.json | 9 ++++++++- .../subcontracting_receipt/subcontracting_receipt.py | 8 ++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 94a2589b98..3824853255 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -204,6 +204,10 @@ frappe.ui.form.on('Subcontracting Receipt Item', { rate(frm) { set_missing_values(frm); }, + + items_remove: function(frm) { + set_missing_values(frm); + } }); frappe.ui.form.on('Subcontracting Receipt Supplied Item', { diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index 4b3cc8365c..95fe087d9a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -40,6 +40,7 @@ "col_break_warehouse", "supplier_warehouse", "items_section", + "get_scrap_items", "items", "section_break0", "total_qty", @@ -626,12 +627,18 @@ "fieldtype": "Check", "label": "Edit Posting Date and Time", "print_hide": 1 + }, + { + "fieldname": "get_scrap_items", + "fieldtype": "Button", + "label": "Get Scrap Items", + "options": "get_scrap_items" } ], "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-06 18:43:16.171842", + "modified": "2023-08-25 10:02:11.546559", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 36be4d1035..5f64ab1323 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -147,6 +147,7 @@ class SubcontractingReceipt(SubcontractingController): ): self.supplied_items = [] + @frappe.whitelist() def get_scrap_items(self): if ( frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") @@ -176,11 +177,16 @@ class SubcontractingReceipt(SubcontractingController): "recalculate_rate": 0, }, ) + else: + self.calculate_additional_costs() + self.calculate_items_qty_and_amount() def remove_scrap_items(self): for item in list(self.items): if item.is_scrap_item: self.remove(item) + else: + item.scrap_cost_per_qty = 0 @frappe.whitelist() def set_missing_values(self): @@ -239,6 +245,8 @@ class SubcontractingReceipt(SubcontractingController): scrap_cost_map[item.reference_name] += item.amount else: scrap_cost_map[item.reference_name] = item.amount + else: + item.scrap_cost_per_qty = 0 total_qty = total_amount = 0 for item in self.items: From 388a42ec7ee0e6a2a36b81c6f3c84b07deb8b12c Mon Sep 17 00:00:00 2001 From: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> Date: Fri, 25 Aug 2023 11:44:05 +0530 Subject: [PATCH 27/60] =?UTF-8?q?fix:=20Asset=20Category=20filter=20is=20n?= =?UTF-8?q?ot=20working=20in=20asset=20depreciation=20and=20b=E2=80=A6=20(?= =?UTF-8?q?#36806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: Asset Category filter is not working in asset depreciation and balances Co-authored-by: ubuntu --- .../asset_depreciations_and_balances.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index d67eee3552..bdc8d8504f 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -58,6 +58,9 @@ def get_data(filters): def get_asset_categories(filters): + condition = "" + if filters.get("asset_category"): + condition += " and asset_category = %(asset_category)s" return frappe.db.sql( """ SELECT asset_category, @@ -98,15 +101,25 @@ def get_asset_categories(filters): 0 end), 0) as cost_of_scrapped_asset from `tabAsset` - where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s + where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {} group by asset_category - """, - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """.format( + condition + ), + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "asset_category": filters.get("asset_category"), + }, as_dict=1, ) def get_assets(filters): + condition = "" + if filters.get("asset_category"): + condition = " and a.asset_category = '{}'".format(filters.get("asset_category")) return frappe.db.sql( """ SELECT results.asset_category, @@ -138,7 +151,7 @@ def get_assets(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} group by a.asset_category union SELECT a.asset_category, @@ -154,10 +167,12 @@ def get_assets(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} group by a.asset_category) as results group by results.asset_category - """, + """.format( + condition + ), {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1, ) From bb7bed4c1a10aa49bbb1d14654454866918cf35c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 20:46:33 +0530 Subject: [PATCH 28/60] refactor(test): make use of mixin in ar/ap report tests --- .../test_accounts_receivable.py | 211 ++++++++---------- erpnext/accounts/test/accounts_mixin.py | 25 ++- 2 files changed, 121 insertions(+), 115 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 6f1889b34e..0099e79e5d 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -8,20 +8,17 @@ from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -class TestAccountsReceivable(FrappeTestCase): +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def setUp(self): - frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") - frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") - frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'") - - self.create_usd_account() + self.create_company() + self.create_customer() + self.create_item() + self.create_usd_receivable_account() + self.clear_old_entries() def tearDown(self): frappe.db.rollback() @@ -49,9 +46,61 @@ class TestAccountsReceivable(FrappeTestCase): debtors_usd.account_type = debtors.account_type self.debtors_usd = debtors_usd.save().name + def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False): + frappe.set_user("Administrator") + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_save=1, + ) + if not no_payment_schedule: + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def create_payment_entry(self, docname): + pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40) + pe.paid_from = self.debit_to + pe.insert() + pe.submit() + + def create_credit_note(self, docname): + credit_note = create_sales_invoice( + company=self.company, + customer=self.customer, + item=self.item, + qty=-1, + debit_to=self.debit_to, + cost_center=self.cost_center, + is_return=1, + return_against=docname, + ) + + return credit_note + def test_accounts_receivable(self): filters = { - "company": "_Test Company 2", + "company": self.company, "based_on_payment_terms": 1, "report_date": today(), "range1": 30, @@ -61,7 +110,9 @@ class TestAccountsReceivable(FrappeTestCase): } # check invoice grand total and invoiced column's value for 3 payment terms - name = make_sales_invoice().name + si = self.create_sales_invoice() + name = si.name + report = execute(filters) expected_data = [[100, 30], [100, 50], [100, 20]] @@ -71,7 +122,7 @@ class TestAccountsReceivable(FrappeTestCase): self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) # check invoice grand total, invoiced, paid and outstanding column's value after payment - make_payment(name) + self.create_payment_entry(si.name) report = execute(filters) expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]] @@ -84,10 +135,10 @@ class TestAccountsReceivable(FrappeTestCase): ) # check invoice grand total, invoiced, paid and outstanding column's value after credit note - make_credit_note(name) + self.create_credit_note(si.name) report = execute(filters) - expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"] + expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to] row = report[1][0] self.assertEqual( @@ -108,21 +159,20 @@ class TestAccountsReceivable(FrappeTestCase): """ so = make_sales_order( - company="_Test Company 2", - customer="_Test Customer 2", - warehouse="Finished Goods - _TC2", - currency="EUR", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", + company=self.company, + customer=self.customer, + warehouse=self.warehouse, + debit_to=self.debit_to, + income_account=self.income_account, + expense_account=self.expense_account, + cost_center=self.cost_center, ) pe = get_payment_entry(so.doctype, so.name) pe = pe.save().submit() filters = { - "company": "_Test Company 2", + "company": self.company, "based_on_payment_terms": 0, "report_date": today(), "range1": 30, @@ -147,34 +197,32 @@ class TestAccountsReceivable(FrappeTestCase): ) @change_settings( - "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, ) def test_exchange_revaluation_for_party(self): """ - Exchange Revaluation for party on Receivable/Payable shoule be included + Exchange Revaluation for party on Receivable/Payable should be included """ - company = "_Test Company 2" - customer = "_Test Customer 2" - # Using Exchange Gain/Loss account for unrealized as well. - company_doc = frappe.get_doc("Company", company) + company_doc = frappe.get_doc("Company", self.company) company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account company_doc.save() - si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) si.currency = "USD" - si.conversion_rate = 0.90 + si.conversion_rate = 80 si.debit_to = self.debtors_usd si = si.save().submit() # Exchange Revaluation err = frappe.new_doc("Exchange Rate Revaluation") - err.company = company + err.company = self.company err.posting_date = today() accounts = err.get_accounts_data() err.extend("accounts", accounts) - err.accounts[0].new_exchange_rate = 0.95 + err.accounts[0].new_exchange_rate = 85 row = err.accounts[0] row.new_balance_in_base_currency = flt( row.new_exchange_rate * flt(row.balance_in_account_currency) @@ -189,7 +237,7 @@ class TestAccountsReceivable(FrappeTestCase): je = je.submit() filters = { - "company": company, + "company": self.company, "report_date": today(), "range1": 30, "range2": 60, @@ -198,7 +246,7 @@ class TestAccountsReceivable(FrappeTestCase): } report = execute(filters) - expected_data_for_err = [0, -5, 0, 5] + expected_data_for_err = [0, -500, 0, 500] row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0] self.assertEqual( expected_data_for_err, @@ -214,46 +262,43 @@ class TestAccountsReceivable(FrappeTestCase): """ Payment against credit/debit note should be considered against the parent invoice """ - company = "_Test Company 2" - customer = "_Test Customer 2" - si1 = make_sales_invoice() + si1 = self.create_sales_invoice() - pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2") - pe.paid_from = "Debtors - _TC2" + pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash) + pe.paid_from = self.debit_to pe.insert() pe.submit() - cr_note = make_credit_note(si1.name) + cr_note = self.create_credit_note(si1.name) - si2 = make_sales_invoice() + si2 = self.create_sales_invoice() # manually link cr_note with si2 using journal entry je = frappe.new_doc("Journal Entry") - je.company = company + je.company = self.company je.voucher_type = "Credit Note" je.posting_date = today() - debit_account = "Debtors - _TC2" debit_entry = { - "account": debit_account, + "account": self.debit_to, "party_type": "Customer", - "party": customer, + "party": self.customer, "debit": 100, "debit_in_account_currency": 100, "reference_type": cr_note.doctype, "reference_name": cr_note.name, - "cost_center": "Main - _TC2", + "cost_center": self.cost_center, } credit_entry = { - "account": debit_account, + "account": self.debit_to, "party_type": "Customer", - "party": customer, + "party": self.customer, "credit": 100, "credit_in_account_currency": 100, "reference_type": si2.doctype, "reference_name": si2.name, - "cost_center": "Main - _TC2", + "cost_center": self.cost_center, } je.append("accounts", debit_entry) @@ -261,7 +306,7 @@ class TestAccountsReceivable(FrappeTestCase): je = je.save().submit() filters = { - "company": company, + "company": self.company, "report_date": today(), "range1": 30, "range2": 60, @@ -270,65 +315,3 @@ class TestAccountsReceivable(FrappeTestCase): } report = execute(filters) self.assertEqual(report[1], []) - - -def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): - frappe.set_user("Administrator") - - si = create_sales_invoice( - company="_Test Company 2", - customer="_Test Customer 2", - currency="EUR", - warehouse="Finished Goods - _TC2", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", - do_not_save=1, - ) - - if not no_payment_schedule: - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), - ) - - si = si.save() - - if not do_not_submit: - si = si.submit() - - return si - - -def make_payment(docname): - pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40) - pe.paid_from = "Debtors - _TC2" - pe.insert() - pe.submit() - - -def make_credit_note(docname): - credit_note = create_sales_invoice( - company="_Test Company 2", - customer="_Test Customer 2", - currency="EUR", - qty=-1, - warehouse="Finished Goods - _TC2", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", - is_return=1, - return_against=docname, - ) - - return credit_note diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index debfffdcbb..bf01362c97 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -60,7 +60,6 @@ class AccountsTestMixin: self.income_account = "Sales - " + abbr self.expense_account = "Cost of Goods Sold - " + abbr self.debit_to = "Debtors - " + abbr - self.debit_usd = "Debtors USD - " + abbr self.cash = "Cash - " + abbr self.creditors = "Creditors - " + abbr self.retained_earnings = "Retained Earnings - " + abbr @@ -105,6 +104,28 @@ class AccountsTestMixin: new_acc.save() setattr(self, acc.attribute_name, new_acc.name) + def create_usd_receivable_account(self): + account_name = "Debtors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Receivable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Receivable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.debtors_usd = acc.name + def clear_old_entries(self): doctype_list = [ "GL Entry", @@ -113,6 +134,8 @@ class AccountsTestMixin: "Purchase Invoice", "Payment Entry", "Journal Entry", + "Sales Order", + "Exchange Rate Revaluation", ] for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() From 879d31a5881490962a0f6e253887076582edbf64 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 25 Aug 2023 16:13:08 +0530 Subject: [PATCH 29/60] fix: recalculate rate while getting scrap items --- .../subcontracting_receipt.js | 29 ++++++++++-- .../subcontracting_receipt.py | 44 +++++++++++-------- .../subcontracting_receipt_item.json | 7 +-- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 3824853255..1a20f0ccf5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -3,7 +3,7 @@ frappe.provide('erpnext.buying'); -erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Receipt"); +erpnext.landed_cost_taxes_and_charges.setup_triggers('Subcontracting Receipt'); frappe.ui.form.on('Subcontracting Receipt', { setup: (frm) => { @@ -77,13 +77,13 @@ frappe.ui.form.on('Subcontracting Receipt', { } }); - frm.set_query("serial_and_batch_bundle", "supplied_items", (doc, cdt, cdn) => { + frm.set_query('serial_and_batch_bundle', 'supplied_items', (doc, cdt, cdn) => { let row = locals[cdt][cdn]; return { filters: { 'item_code': row.rm_item_code, 'voucher_type': doc.doctype, - 'voucher_no': ["in", [doc.name, ""]], + 'voucher_no': ['in', [doc.name, '']], 'is_cancelled': 0, } } @@ -180,6 +180,23 @@ frappe.ui.form.on('Subcontracting Receipt', { rejected_warehouse: (frm) => { set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse); }, + + get_scrap_items: (frm) => { + frappe.call({ + doc: frm.doc, + method: 'get_scrap_items', + args: { + recalculate_rate: true + }, + freeze: true, + freeze_message: __('Getting Scrap Items'), + callback: (r) => { + if (!r.exc) { + frm.refresh(); + } + } + }); + }, }); frappe.ui.form.on('Landed Cost Taxes and Charges', { @@ -205,6 +222,12 @@ frappe.ui.form.on('Subcontracting Receipt Item', { set_missing_values(frm); }, + recalculate_rate(frm) { + if (frm.doc.recalculate_rate) { + set_missing_values(frm); + } + }, + items_remove: function(frm) { set_missing_values(frm); } diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 5f64ab1323..09db66f97a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -148,7 +148,7 @@ class SubcontractingReceipt(SubcontractingController): self.supplied_items = [] @frappe.whitelist() - def get_scrap_items(self): + def get_scrap_items(self, recalculate_rate=False): if ( frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") == "BOM" @@ -163,42 +163,43 @@ class SubcontractingReceipt(SubcontractingController): self.append( "items", { + "is_scrap_item": 1, + "reference_name": item.name, "item_code": scrap_item.item_code, "item_name": scrap_item.item_name, "qty": qty, "stock_uom": scrap_item.stock_uom, + "recalculate_rate": 1, "rate": scrap_item.rate, + "rm_cost_per_qty": 0, + "service_cost_per_qty": 0, + "additional_cost_per_qty": 0, + "scrap_cost_per_qty": scrap_item.rate, "amount": qty * scrap_item.rate, - "is_scrap_item": 1, "warehouse": self.set_warehouse, "rejected_warehouse": self.rejected_warehouse, - "service_cost_per_qty": 0, - "reference_name": item.name, - "recalculate_rate": 0, }, ) - else: + + if recalculate_rate: self.calculate_additional_costs() self.calculate_items_qty_and_amount() - def remove_scrap_items(self): + def remove_scrap_items(self, recalculate_rate=False): for item in list(self.items): if item.is_scrap_item: self.remove(item) else: item.scrap_cost_per_qty = 0 + if recalculate_rate: + self.calculate_items_qty_and_amount() + @frappe.whitelist() def set_missing_values(self): - self.calculate_additional_costs() - self.calculate_supplied_items_qty_and_amount() - self.calculate_items_qty_and_amount() - - def calculate_supplied_items_qty_and_amount(self): - for item in self.get("supplied_items") or []: - item.amount = item.rate * item.consumed_qty - self.set_available_qty_for_consumption() + self.calculate_additional_costs() + self.calculate_items_qty_and_amount() def set_available_qty_for_consumption(self): supplied_items_details = {} @@ -233,6 +234,8 @@ class SubcontractingReceipt(SubcontractingController): def calculate_items_qty_and_amount(self): rm_cost_map = {} for item in self.get("supplied_items") or []: + item.amount = flt(item.consumed_qty) * flt(item.rate) + if item.reference_name in rm_cost_map: rm_cost_map[item.reference_name] += item.amount else: @@ -241,15 +244,18 @@ class SubcontractingReceipt(SubcontractingController): scrap_cost_map = {} for item in self.get("items") or []: if item.is_scrap_item: + if item.recalculate_rate: + item.rate = flt(item.scrap_cost_per_qty) + flt(item.additional_cost_per_qty) + + item.amount = flt(item.qty) * flt(item.rate) + if item.reference_name in scrap_cost_map: scrap_cost_map[item.reference_name] += item.amount else: scrap_cost_map[item.reference_name] = item.amount - else: - item.scrap_cost_per_qty = 0 total_qty = total_amount = 0 - for item in self.items: + for item in self.get("items") or []: if not item.is_scrap_item: if item.qty: if item.name in rm_cost_map: @@ -260,6 +266,8 @@ class SubcontractingReceipt(SubcontractingController): if item.name in scrap_cost_map: item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty scrap_cost_map.pop(item.name) + else: + item.scrap_cost_per_qty = 0 if item.recalculate_rate: item.rate = ( diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 5442dfddd7..b333627b4a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -476,11 +476,9 @@ }, { "default": "1", - "depends_on": "eval: !doc.is_scrap_item", "fieldname": "recalculate_rate", "fieldtype": "Check", - "label": "Recalculate Rate", - "read_only_depends_on": "eval: doc.is_scrap_item" + "label": "Recalculate Rate" }, { "fieldname": "serial_and_batch_bundle", @@ -510,7 +508,6 @@ }, { "default": "0", - "depends_on": "eval: !doc.is_scrap_item", "fieldname": "scrap_cost_per_qty", "fieldtype": "Float", "label": "Scrap Cost Per Qty", @@ -530,7 +527,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-08-25 09:33:47.232140", + "modified": "2023-08-25 15:42:36.923833", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From ce81ffd84438a9fe9df251507adea50e1f55f089 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 25 Aug 2023 15:41:18 +0530 Subject: [PATCH 30/60] test: increase coverage in ar/ap report --- .../test_accounts_receivable.py | 257 +++++++++++++++++- 1 file changed, 255 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 0099e79e5d..0c7d931d2d 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -107,6 +107,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "show_remarks": True, } # check invoice grand total and invoiced column's value for 3 payment terms @@ -115,11 +116,11 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): report = execute(filters) - expected_data = [[100, 30], [100, 50], [100, 20]] + expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]] for i in range(3): row = report[1][i - 1] - self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) # check invoice grand total, invoiced, paid and outstanding column's value after payment self.create_payment_entry(si.name) @@ -315,3 +316,255 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): } report = execute(filters) self.assertEqual(report[1], []) + + def test_group_by_party(self): + si1 = self.create_sales_invoice(do_not_submit=True) + si1.posting_date = add_days(today(), -1) + si1.save().submit() + si2 = self.create_sales_invoice(do_not_submit=True) + si2.items[0].rate = 85 + si2.save().submit() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "group_by_party": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 5) + + # assert voucher rows + expected_voucher_rows = [ + [100.0, 100.0, 100.0, 100.0], + [85.0, 85.0, 85.0, 85.0], + ] + voucher_rows = [] + for x in report[0:2]: + voucher_rows.append( + [x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency] + ) + self.assertEqual(expected_voucher_rows, voucher_rows) + + # assert total rows + expected_total_rows = [ + [self.customer, 185.0, 185.0], # party total + {}, # empty row for padding + ["Total", 185.0, 185.0], # grand total + ] + party_total_row = report[2] + self.assertEqual( + expected_total_rows[0], + [ + party_total_row.get("party"), + party_total_row.get("invoiced"), + party_total_row.get("outstanding"), + ], + ) + empty_row = report[3] + self.assertEqual(expected_total_rows[1], empty_row) + grand_total_row = report[4] + self.assertEqual( + expected_total_rows[2], + [ + grand_total_row.get("party"), + grand_total_row.get("invoiced"), + grand_total_row.get("outstanding"), + ], + ) + + def test_future_payments(self): + si = self.create_sales_invoice() + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.paid_amount = 90.0 + pe.references[0].allocated_amount = 90.0 + pe.save().submit() + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_future_payments": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [100.0, 100.0, 10.0, 90.0] + + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # full payment in future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.save().submit() + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 0.0, 100.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # over payment in future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.paid_amount = 110 + pe.save().submit() + report = execute(filters)[1] + self.assertEqual(len(report), 2) + expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]] + for idx, row in enumerate(report): + self.assertEqual( + expected_data[idx], + [row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount], + ) + + def test_sales_person(self): + sales_person = ( + frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}) + .insert() + .submit() + ) + si = self.create_sales_invoice(do_not_submit=True) + si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100}) + si.save().submit() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "sales_person": sales_person.name, + "show_sales_person": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [100.0, 100.0, sales_person.name] + + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person]) + + def test_cost_center_filter(self): + si = self.create_sales_invoice() + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "cost_center": self.cost_center, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, self.cost_center] + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center]) + + def test_customer_group_filter(self): + si = self.create_sales_invoice() + cus_group = frappe.db.get_value("Customer", self.customer, "customer_group") + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "customer_group": cus_group, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, cus_group] + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group]) + + filters.update({"customer_group": "Individual"}) + report = execute(filters)[1] + self.assertEqual(len(report), 0) + + def test_party_account_filter(self): + si1 = self.create_sales_invoice() + self.customer2 = ( + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "Jane Doe", + "type": "Individual", + "default_currency": "USD", + } + ) + .insert() + .submit() + ) + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.posting_date = add_days(today(), -1) + si2.customer = self.customer2 + si2.currency = "USD" + si2.conversion_rate = 80 + si2.debit_to = self.debtors_usd + si2.save().submit() + + # Filter on company currency receivable account + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "party_account": self.debit_to, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, self.debit_to, si1.currency] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] + ) + + # Filter on USD receivable account + filters.update({"party_account": self.debtors_usd}) + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] + ) + + # without filter on party account + filters.pop("party_account") + report = execute(filters)[1] + self.assertEqual(len(report), 2) + expected_data = [ + [8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency], + [100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency], + ] + for idx, row in enumerate(report): + self.assertEqual( + expected_data[idx], + [ + row.invoiced, + row.outstanding, + row.invoiced_in_account_currency, + row.outstanding_in_account_currency, + row.party_account, + row.account_currency, + ], + ) From 1504ff8b6c299a10ca7e4af263561a83bb6d3d9c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 25 Aug 2023 20:18:30 +0530 Subject: [PATCH 31/60] fix: ignore scrap items while distribute additional cost --- .../controllers/subcontracting_controller.py | 19 ++++++++++++------- .../subcontracting_receipt.js | 7 ++++++- .../subcontracting_receipt.py | 7 ++----- .../subcontracting_receipt_item.json | 8 ++++++-- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 837969728b..913c80b26a 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -895,19 +895,24 @@ class SubcontractingController(StockController): if self.total_additional_costs: if self.distribute_additional_costs_based_on == "Amount": - total_amt = sum(flt(item.amount) for item in self.get("items")) + total_amt = sum( + flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item") + ) for item in self.items: - item.additional_cost_per_qty = ( - (item.amount * self.total_additional_costs) / total_amt - ) / item.qty + if not item.get("is_scrap_item"): + item.additional_cost_per_qty = ( + (item.amount * self.total_additional_costs) / total_amt + ) / item.qty else: - total_qty = sum(flt(item.qty) for item in self.get("items")) + total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item")) additional_cost_per_qty = self.total_additional_costs / total_qty for item in self.items: - item.additional_cost_per_qty = additional_cost_per_qty + if not item.get("is_scrap_item"): + item.additional_cost_per_qty = additional_cost_per_qty else: for item in self.items: - item.additional_cost_per_qty = 0 + if not item.get("is_scrap_item"): + item.additional_cost_per_qty = 0 @frappe.whitelist() def get_current_stock(self): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 1a20f0ccf5..8edbd4af08 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -201,12 +201,17 @@ frappe.ui.form.on('Subcontracting Receipt', { frappe.ui.form.on('Landed Cost Taxes and Charges', { amount: function (frm, cdt, cdn) { + set_missing_values(frm); frm.events.set_base_amount(frm, cdt, cdn); }, expense_account: function (frm, cdt, cdn) { frm.events.set_account_currency(frm, cdt, cdn); - } + }, + + additional_costs_remove: function(frm) { + set_missing_values(frm); + } }); frappe.ui.form.on('Subcontracting Receipt Item', { diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 09db66f97a..b28292f0a4 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -169,12 +169,12 @@ class SubcontractingReceipt(SubcontractingController): "item_name": scrap_item.item_name, "qty": qty, "stock_uom": scrap_item.stock_uom, - "recalculate_rate": 1, + "recalculate_rate": 0, "rate": scrap_item.rate, "rm_cost_per_qty": 0, "service_cost_per_qty": 0, "additional_cost_per_qty": 0, - "scrap_cost_per_qty": scrap_item.rate, + "scrap_cost_per_qty": 0, "amount": qty * scrap_item.rate, "warehouse": self.set_warehouse, "rejected_warehouse": self.rejected_warehouse, @@ -244,9 +244,6 @@ class SubcontractingReceipt(SubcontractingController): scrap_cost_map = {} for item in self.get("items") or []: if item.is_scrap_item: - if item.recalculate_rate: - item.rate = flt(item.scrap_cost_per_qty) + flt(item.additional_cost_per_qty) - item.amount = flt(item.qty) * flt(item.rate) if item.reference_name in scrap_cost_map: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index b333627b4a..c036390ba3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -237,6 +237,7 @@ }, { "default": "0", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "additional_cost_per_qty", "fieldtype": "Currency", "label": "Additional Cost Per Qty", @@ -476,9 +477,11 @@ }, { "default": "1", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "recalculate_rate", "fieldtype": "Check", - "label": "Recalculate Rate" + "label": "Recalculate Rate", + "read_only_depends_on": "eval: doc.is_scrap_item" }, { "fieldname": "serial_and_batch_bundle", @@ -508,6 +511,7 @@ }, { "default": "0", + "depends_on": "eval: !doc.is_scrap_item", "fieldname": "scrap_cost_per_qty", "fieldtype": "Float", "label": "Scrap Cost Per Qty", @@ -527,7 +531,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-08-25 15:42:36.923833", + "modified": "2023-08-25 20:09:03.069417", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From c8d7433e30e91e6b99c75ebe791827989906d9f8 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 25 Aug 2023 20:23:39 +0530 Subject: [PATCH 32/60] refactor(minor): `subcontracting_receipt.js` --- .../subcontracting_receipt.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 8edbd4af08..a890437752 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -52,14 +52,14 @@ frappe.ui.form.on('Subcontracting Receipt', { } })); - frm.set_query('expense_account', 'items', function () { + frm.set_query('expense_account', 'items', () => { return { query: 'erpnext.controllers.queries.get_expense_account', filters: { 'company': frm.doc.company } }; }); - frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { + frm.set_query('batch_no', 'items', (doc, cdt, cdn) => { var row = locals[cdt][cdn]; return { filters: { @@ -68,7 +68,7 @@ frappe.ui.form.on('Subcontracting Receipt', { } }); - frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) { + frm.set_query('batch_no', 'supplied_items', (doc, cdt, cdn) => { var row = locals[cdt][cdn]; return { filters: { @@ -101,7 +101,7 @@ frappe.ui.form.on('Subcontracting Receipt', { let batch_no_field = frm.get_docfield('items', 'batch_no'); if (batch_no_field) { - batch_no_field.get_route_options_for_new_doc = function(row) { + batch_no_field.get_route_options_for_new_doc = (row) => { return { 'item': row.doc.item_code } @@ -111,7 +111,7 @@ frappe.ui.form.on('Subcontracting Receipt', { refresh: (frm) => { if (frm.doc.docstatus > 0) { - frm.add_custom_button(__('Stock Ledger'), function () { + frm.add_custom_button(__('Stock Ledger'), () => { frappe.route_options = { voucher_no: frm.doc.name, from_date: frm.doc.posting_date, @@ -122,7 +122,7 @@ frappe.ui.form.on('Subcontracting Receipt', { frappe.set_route('query-report', 'Stock Ledger'); }, __('View')); - frm.add_custom_button(__('Accounting Ledger'), function () { + frm.add_custom_button(__('Accounting Ledger'), () => { frappe.route_options = { voucher_no: frm.doc.name, from_date: frm.doc.posting_date, @@ -136,7 +136,7 @@ frappe.ui.form.on('Subcontracting Receipt', { } if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) { - frm.add_custom_button(__('Subcontract Return'), function () { + frm.add_custom_button(__('Subcontract Return'), () => { frappe.model.open_mapped_doc({ method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return', frm: frm @@ -146,7 +146,7 @@ frappe.ui.form.on('Subcontracting Receipt', { } if (frm.doc.docstatus == 0) { - frm.add_custom_button(__('Subcontracting Order'), function () { + frm.add_custom_button(__('Subcontracting Order'), () => { if (!frm.doc.supplier) { frappe.throw({ title: __('Mandatory'), @@ -200,18 +200,18 @@ frappe.ui.form.on('Subcontracting Receipt', { }); frappe.ui.form.on('Landed Cost Taxes and Charges', { - amount: function (frm, cdt, cdn) { + amount: (frm, cdt, cdn) => { set_missing_values(frm); frm.events.set_base_amount(frm, cdt, cdn); }, - expense_account: function (frm, cdt, cdn) { + expense_account: (frm, cdt, cdn) => { frm.events.set_account_currency(frm, cdt, cdn); }, - additional_costs_remove: function(frm) { - set_missing_values(frm); - } + additional_costs_remove: (frm) => { + set_missing_values(frm); + } }); frappe.ui.form.on('Subcontracting Receipt Item', { @@ -233,9 +233,9 @@ frappe.ui.form.on('Subcontracting Receipt Item', { } }, - items_remove: function(frm) { - set_missing_values(frm); - } + items_remove: (frm) => { + set_missing_values(frm); + } }); frappe.ui.form.on('Subcontracting Receipt Supplied Item', { From e462edc6282f6e95b948f4f2583dabd96a92ca62 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Fri, 25 Aug 2023 17:10:31 +0200 Subject: [PATCH 33/60] chore: update fr translation for Naming Series (#36785) * chore: update fr translation for Naming Series * chore: update fr translation * chore: update fr translation * chore: update fr translation --- erpnext/translations/fr.csv | 38 ++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 801604a4d8..37795261a9 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -3279,7 +3279,7 @@ Quality Feedback,Commentaires sur la qualité, Quality Feedback Template,Modèle de commentaires sur la qualité, Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels., Show {0},Montrer {0}, -"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les séries de nommage {0}", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les masques de numérotation {0}", Target Details,Détails de la cible, {0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}., API,API, @@ -3292,7 +3292,7 @@ Group By,Par groupe, Invalid URL,URL invalide, Landscape,Paysage, Last Sync On,Dernière synchronisation le, -Naming Series,Nom de série, +Naming Series,Masque de numérotation, No data to export,Aucune donnée à exporter, Portrait,Portrait, Print Heading,Imprimer Titre, @@ -3962,7 +3962,7 @@ Please set {0},Veuillez définir {0},supplier Draft,Brouillon,"docstatus,=,0" Cancelled,Annulé,"docstatus,=,2" Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation> Paramètres de l'éducation, -Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration> Paramètres> Série de noms, +Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir le masque de numérotation pour {0} via Configuration> Paramètres> Série de noms, UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -> {1}) introuvable pour l'article: {2}, Item Code > Item Group > Brand,Code article> Groupe d'articles> Marque, Customer > Customer Group > Territory,Client> Groupe de clients> Territoire, @@ -3973,7 +3973,7 @@ Fetch Serial Numbers based on FIFO,Récupérer les numéros de série basés sur "Outward taxable supplies(other than zero rated, nil rated and exempted)","Fournitures taxables sortantes (autres que détaxées, nulles et exonérées)", "To allow different rates, disable the {0} checkbox in {1}.","Pour autoriser différents tarifs, désactivez la {0} case à cocher dans {1}.", Asset{} {assets_link} created for {},Élément {} {assets_link} créé pour {}, -Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: la série de noms d'éléments est obligatoire pour la création automatique de l'élément {}, +Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: Le masque de numérotation d'éléments est obligatoire pour la création automatique de l'élément {}, Assets not created for {0}. You will have to create asset manually.,Éléments non créés pour {0}. Vous devrez créer un actif manuellement., {0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} a des écritures comptables dans la devise {2} pour l'entreprise {3}. Veuillez sélectionner un compte à recevoir ou à payer avec la devise {2}., Invalid Account,Compte invalide, @@ -3997,7 +3997,7 @@ Advanced Settings,Réglages avancés, Path,Chemin, Components,Composants, Verified By,Vérifié Par, -Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0}, +Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0}, Filter Based On,Filtre basé sur, Reqd by date,Reqd par date, Manufacturer Part Number {0} is invalid,Le numéro de pièce du fabricant {0} n'est pas valide, @@ -5587,7 +5587,7 @@ Student Admission Program,Programme d'admission des étudiants, Minimum Age,Âge Minimum, Maximum Age,Âge Maximum, Application Fee,Frais de Dossier, -Naming Series (for Student Applicant),Nom de série (pour un candidat étudiant), +Naming Series (for Student Applicant),Masque de numérotation (pour un candidat étudiant), LMS Only,LMS seulement, EDU-APP-.YYYY.-,EDU-APP-YYYY.-, Application Date,Date de la Candidature, @@ -6074,7 +6074,7 @@ Hotel Reservation User,Utilisateur chargé des réservations d'hôtel, Hotel Room Reservation Item,Article de réservation de la chambre d'hôtel, Hotel Settings,Paramètres d'Hotel, Default Taxes and Charges,Taxes et frais par défaut, -Default Invoice Naming Series,Numéro de série par défaut pour les factures, +Default Invoice Naming Series,Masque de numérotation par défaut pour les factures, HR,RH, Date on which this component is applied,Date à laquelle ce composant est appliqué, Salary Slip,Fiche de Paie, @@ -7136,7 +7136,7 @@ Default Unit of Measure,Unité de Mesure par Défaut, Maintain Stock,Maintenir Stock, Standard Selling Rate,Prix de Vente Standard, Auto Create Assets on Purchase,Création automatique d'actifs à l'achat, -Asset Naming Series,Nom de série de l'actif, +Asset Naming Series,Masque de numérotation de l'actif, Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%), Barcodes,Codes-barres, Shelf Life In Days,Durée de conservation en jours, @@ -7155,7 +7155,7 @@ Serial Nos and Batches,N° de Série et Lots, Has Batch No,A un Numéro de Lot, Automatically Create New Batch,Créer un Nouveau Lot Automatiquement, Batch Number Series,Série de numéros de lots, -"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.", +"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si le masque est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec ce masque. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe du masque dans les paramètres de stock.", Has Expiry Date,A une date d'expiration, Retain Sample,Conserver l'échantillon, Max Sample Quantity,Quantité maximum d'échantillon, @@ -7455,8 +7455,8 @@ Inter Warehouse Transfer Settings,Paramètres de transfert entre entrepôts, Freeze Stock Entries,Geler les Entrées de Stocks, Stock Frozen Upto,Stock Gelé Jusqu'au, Batch Identification,Identification par lots, -Use Naming Series,Utiliser la série de noms, -Naming Series Prefix,Préfix du nom de série, +Use Naming Series,Utiliser le masque de numérotation, +Naming Series Prefix,Préfix du masque de numérotation, UOM Category,Catégorie d'unité de mesure (UdM), UOM Conversion Detail,Détails de Conversion de l'UdM, Variant Field,Champ de Variante, @@ -7914,7 +7914,7 @@ Is Inter State,Est Inter State, Purchase Details,Détails d'achat, Depreciation Posting Date,Date comptable de l'amortissement, "By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Par défaut, le nom du fournisseur est défini selon le nom du fournisseur saisi. Si vous souhaitez que les fournisseurs soient nommés par un", - choose the 'Naming Series' option.,choisissez l'option 'Naming Series'., + choose the 'Naming Series' option.,choisissez l'option 'Masque de numérotation'., Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Configurez la liste de prix par défaut lors de la création d'une nouvelle transaction d'achat. Les prix des articles seront extraits de cette liste de prix., "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat ou un reçu sans créer d'abord une Commande d'Achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case «Autoriser la création de facture d'achat sans commmande d'achat» dans la fiche fournisseur.", "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat sans créer d'abord un reçu d'achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case "Autoriser la création de facture d'achat sans reçu d'achat" dans la fiche fournisseur.", @@ -8871,7 +8871,7 @@ Auto Insert Item Price If Missing,Création du prix de l'article dans les listes Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix, Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock, Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions, -Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries, +Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries, "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" Allowed Items,Articles autorisés, Party Specific Item,Restriction d'article disponible, @@ -8925,3 +8925,15 @@ Enable Reviews and Ratings,Activer les avis et notes, Enable Recommendations,Activer les recommendations, Item Search Settings,Paramétrage de la recherche d'article, Purchase demande,Demande de materiel, +Internal Customer,Client interne +Internal Supplier,Fournisseur interne +Contact & Address,Contact et Adresse +Primary Address and Contact,Adresse et contact principal +Supplier Primary Contact,Contact fournisseur principal +Supplier Primary Address,Adresse fournisseur principal +From Opportunity,Depuis l'opportunité +Default Receivable Accounts,Compte de débit par défaut +Receivable Accounts,Compte de débit +Mention if a non-standard receivable account,Veuillez mentionner s'il s'agit d'un compte débiteur non standard +Allow Purchase,Autoriser à l'achat +Inventory Settings,Paramétrage de l'inventaire From a7137514b69caac2a1371d080e902ea982269aa0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Aug 2023 21:05:49 +0530 Subject: [PATCH 34/60] chore: Update patch --- erpnext/patches/v14_0/delete_education_doctypes.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py index 468c2fda32..56a596a02e 100644 --- a/erpnext/patches/v14_0/delete_education_doctypes.py +++ b/erpnext/patches/v14_0/delete_education_doctypes.py @@ -43,14 +43,17 @@ def execute(): frappe.delete_doc("Number Card", card, ignore_missing=True, force=True) doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name") + for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) - portal_menu_items = frappe.get_all( - "Portal Menu Item", {"reference_doctype": ("in", doctypes)}, pluck="name" - ) - for menu_item in portal_menu_items: - frappe.delete_doc("Portal Menu Item", menu_item, ignore_missing=True, force=True) + portal_settings = frappe.get_doc("Portal Settings") + + for row in portal_settings.get("menu"): + if row.reference_doctype in doctypes: + row.delete() + + portal_settings.save() frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True) From 08bc33689c8d8125f1c80bb621e46a5a83bfd45f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 26 Aug 2023 10:57:49 +0530 Subject: [PATCH 35/60] fix(ux): make `Get Scrap Items` button hidden for submitted and cancelled doc --- .../subcontracting_receipt/subcontracting_receipt.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index 95fe087d9a..8be1c1ba97 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -286,7 +286,7 @@ "reqd": 1 }, { - "depends_on": "supplied_items", + "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0 && doc.supplied_items)", "fieldname": "get_current_stock", "fieldtype": "Button", "label": "Get Current Stock", @@ -629,6 +629,7 @@ "print_hide": 1 }, { + "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0)", "fieldname": "get_scrap_items", "fieldtype": "Button", "label": "Get Scrap Items", @@ -638,7 +639,7 @@ "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-25 10:02:11.546559", + "modified": "2023-08-26 10:52:04.050829", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", From ed4f498704486664d9810193c5bc01bbfcf4b23e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 26 Aug 2023 11:38:45 +0530 Subject: [PATCH 36/60] feat: allow scrap items when backflush based on is Material Transfer --- .../subcontracting_receipt.py | 62 +++++++++---------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index b28292f0a4..4ab0c666c7 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -149,41 +149,37 @@ class SubcontractingReceipt(SubcontractingController): @frappe.whitelist() def get_scrap_items(self, recalculate_rate=False): - if ( - frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") - == "BOM" - ): - self.remove_scrap_items() + self.remove_scrap_items() - for item in list(self.items): - if item.bom: - bom = frappe.get_doc("BOM", item.bom) - for scrap_item in bom.scrap_items: - qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) - self.append( - "items", - { - "is_scrap_item": 1, - "reference_name": item.name, - "item_code": scrap_item.item_code, - "item_name": scrap_item.item_name, - "qty": qty, - "stock_uom": scrap_item.stock_uom, - "recalculate_rate": 0, - "rate": scrap_item.rate, - "rm_cost_per_qty": 0, - "service_cost_per_qty": 0, - "additional_cost_per_qty": 0, - "scrap_cost_per_qty": 0, - "amount": qty * scrap_item.rate, - "warehouse": self.set_warehouse, - "rejected_warehouse": self.rejected_warehouse, - }, - ) + for item in list(self.items): + if item.bom: + bom = frappe.get_doc("BOM", item.bom) + for scrap_item in bom.scrap_items: + qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) + self.append( + "items", + { + "is_scrap_item": 1, + "reference_name": item.name, + "item_code": scrap_item.item_code, + "item_name": scrap_item.item_name, + "qty": qty, + "stock_uom": scrap_item.stock_uom, + "recalculate_rate": 0, + "rate": scrap_item.rate, + "rm_cost_per_qty": 0, + "service_cost_per_qty": 0, + "additional_cost_per_qty": 0, + "scrap_cost_per_qty": 0, + "amount": qty * scrap_item.rate, + "warehouse": self.set_warehouse, + "rejected_warehouse": self.rejected_warehouse, + }, + ) - if recalculate_rate: - self.calculate_additional_costs() - self.calculate_items_qty_and_amount() + if recalculate_rate: + self.calculate_additional_costs() + self.calculate_items_qty_and_amount() def remove_scrap_items(self, recalculate_rate=False): for item in list(self.items): From d4218a88ad0776d33ebeb0c221fffab432b22dbc Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sat, 26 Aug 2023 12:54:13 +0530 Subject: [PATCH 37/60] chore: fixed bom creator linter issue (#36827) --- .../public/js/bom_configurator/bom_configurator.bundle.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index b3b2e9f9b8..582b487966 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -383,7 +383,7 @@ class BOMConfigurator { frappe.views.trees["BOM Configurator"].tree.load_children(node); - while (true) { + while (node) { item_row = response.message.items.filter(item => item.name === node.data.name); if (item_row?.length) { @@ -402,12 +402,8 @@ class BOMConfigurator { ); $($(parent_dom).find(".fg-item-amt")[0]).html(total_amount); - - if (node.is_root) { - break; - } - node = node.parent_node; + } } From afd7d59c733ff581ba4b7eb1aaf252abc8792984 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 26 Aug 2023 13:10:47 +0530 Subject: [PATCH 38/60] fix: `AttributeError` while saving subcontracting order --- erpnext/controllers/subcontracting_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 913c80b26a..d4270a76d4 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -97,7 +97,7 @@ class SubcontractingController(StockController): if not is_stock_item: frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name)) - if not item.is_scrap_item: + if not item.get("is_scrap_item"): if not is_sub_contracted_item: frappe.throw( _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) From 740fe95ff3869abaf62a6a4c1b26817baef7d7ec Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 26 Aug 2023 18:13:16 +0530 Subject: [PATCH 39/60] fix: error list index out of range when saving a production plan fix: error list index out of range when saving a production plan (#36807) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 131f438e20..34e94232c4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -53,7 +53,7 @@ class ProductionPlan(Document): data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders}) title = _("Production Plan Already Submitted") - if not data: + if not data and sales_orders: msg = _("No items are available in the sales order {0} for production").format(sales_orders[0]) if len(sales_orders) > 1: sales_orders = ", ".join(sales_orders) From 9bc5952dd55fd1dc8d2c8061e325c202793f81d3 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 26 Aug 2023 18:14:40 +0530 Subject: [PATCH 40/60] fix: missing company flag for regional fn (#36791) * fix: missing company flag for regional fn --- erpnext/controllers/accounts_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 955ebef003..1d50639043 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -201,9 +201,9 @@ class AccountsController(TransactionBase): # apply tax withholding only if checked and applicable self.set_tax_withholding() - validate_regional(self) - - validate_einvoice_fields(self) + with temporary_flag("company", self.company): + validate_regional(self) + validate_einvoice_fields(self) if self.doctype != "Material Request" and not self.ignore_pricing_rule: apply_pricing_rule_on_transaction(self) From fa2e5d5ab7f3b9ec79d8ca22640f685391a628a4 Mon Sep 17 00:00:00 2001 From: avc <94137451+git-avc@users.noreply.github.com> Date: Sat, 26 Aug 2023 14:57:42 +0200 Subject: [PATCH 41/60] fix: create portal user for customer from shopping cart (#36745) fix: create portal user for customer from shopping cart --- erpnext/e_commerce/shopping_cart/cart.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 4c82393684..85d9a6585c 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -517,6 +517,8 @@ def get_party(user=None): } ) + customer.append("portal_users", {"user": user}) + if debtors_account: customer.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]}) From 35f72a17db1455a7c1ac082025b6d2ae9fdfe36a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 26 Aug 2023 19:29:33 +0530 Subject: [PATCH 42/60] Allow to make return against sales invoice which has closed sales order. (#36676) fix: Allow to make return against sales invoice which has closed sales order fix: Allow to make return against sales invoice which has closed sales order (cherry picked from commit 0f98cc85e9dce00fa333474cfb438f243503df81) Co-authored-by: Gourav Saini <63018500+gouravsaini021@users.noreply.github.com> --- erpnext/controllers/selling_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 6f1a50dab1..9771f60ceb 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -388,7 +388,7 @@ class SellingController(StockController): for d in self.get("items"): if d.get(ref_fieldname): status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status") - if status in ("Closed", "On Hold"): + if status in ("Closed", "On Hold") and not self.is_return: frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status)) def update_reserved_qty(self): @@ -404,7 +404,9 @@ class SellingController(StockController): if so and so_item_rows: sales_order = frappe.get_doc("Sales Order", so) - if sales_order.status in ["Closed", "Cancelled"]: + if (sales_order.status == "Closed" and not self.is_return) or sales_order.status in [ + "Cancelled" + ]: frappe.throw( _("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError ) From 6b75c03f344ecce9a3d4decacd4fed067c696894 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 26 Aug 2023 17:07:28 +0300 Subject: [PATCH 43/60] chore: translate currency (#36777) chore: translate currency --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ac31e8a1db..032aa80ff2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -999,14 +999,14 @@ class PaymentEntry(AccountsController): if self.payment_type == "Internal Transfer": remarks = [ _("Amount {0} {1} transferred from {2} to {3}").format( - self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to + _(self.paid_from_account_currency), self.paid_amount, self.paid_from, self.paid_to ) ] else: remarks = [ _("Amount {0} {1} {2} {3}").format( - self.party_account_currency, + _(self.party_account_currency), self.paid_amount if self.payment_type == "Receive" else self.received_amount, _("received from") if self.payment_type == "Receive" else _("to"), self.party, @@ -1023,14 +1023,14 @@ class PaymentEntry(AccountsController): if d.allocated_amount: remarks.append( _("Amount {0} {1} against {2} {3}").format( - self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name + _(self.party_account_currency), d.allocated_amount, d.reference_doctype, d.reference_name ) ) for d in self.get("deductions"): if d.amount: remarks.append( - _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account) + _("Amount {0} {1} deducted against {2}").format(_(self.company_currency), d.amount, d.account) ) self.set("remarks", "\n".join(remarks)) From 3f077479f4c564a4bc7764a73d4faa334d6b125f Mon Sep 17 00:00:00 2001 From: L Mwangi Date: Sat, 26 Aug 2023 17:15:55 +0300 Subject: [PATCH 44/60] fix: On woocommerce test ping issued during a webhook creation fix: On woocommerce test ping issued during a webhook creation --- .../connectors/woocommerce_connection.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 6d977e022f..b140df2068 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -41,7 +41,10 @@ def _order(*args, **kwargs): if frappe.flags.woocomm_test_order_data: order = frappe.flags.woocomm_test_order_data event = "created" - + # Ignore the test ping issued during WooCommerce webhook configuration + # Ref: https://github.com/woocommerce/woocommerce/issues/15642 + if frappe.request.data.decode('utf-8').startswith('webhook_id='): + return "success" elif frappe.request and frappe.request.data: verify_request() try: @@ -81,7 +84,9 @@ def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name customer.save() if customer_exists: - frappe.rename_doc("Customer", old_name, customer_name) + # Fixes https://github.com/frappe/erpnext/issues/33708 + if old_name != customer_name: + frappe.rename_doc("Customer", old_name, customer_name) for address_type in ( "Billing", "Shipping", From d40504b9731bbeaf153e87b0d8d765a17fa3c43e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 27 Aug 2023 08:29:53 +0530 Subject: [PATCH 45/60] test: Exchange Rate Revaluation functions and its impact on ledger --- .../test_exchange_rate_revaluation.py | 294 +++++++++++++++++- 1 file changed, 292 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index ec55e60fd1..ced04ced3f 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -3,6 +3,296 @@ import unittest +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, today -class TestExchangeRateRevaluation(unittest.TestCase): - pass +from erpnext import get_default_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.party import get_party_account +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.stock.doctype.item.test_item import create_item + + +class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_usd_receivable_account() + self.create_item() + self.create_customer() + self.clear_old_entries() + self.set_system_and_company_settings() + + def tearDown(self): + frappe.db.rollback() + + def set_system_and_company_settings(self): + # set number and currency precision + system_settings = frappe.get_doc("System Settings") + system_settings.float_precision = 2 + system_settings.currency_precision = 2 + system_settings.save() + + # Using Exchange Gain/Loss account for unrealized as well. + company_doc = frappe.get_doc("Company", self.company) + company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account + company_doc.save() + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_01_revaluation_of_forex_balance(self): + """ + Test Forex account balance and Journal creation post Revaluation + """ + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = (self.company,) + err.posting_date = today() + accounts = err.get_accounts_data() + err.extend("accounts", accounts) + row = err.accounts[0] + row.new_exchange_rate = 85 + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + err.set_total_gain_loss() + err = err.save().submit() + + # Create JV for ERR + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv")) + je = je.submit() + + je.reload() + self.assertEqual(je.voucher_type, "Exchange Rate Revaluation") + self.assertEqual(je.total_debit, 8500.0) + self.assertEqual(je.total_credit, 8500.0) + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=["sum(debit)-sum(credit) as balance"], + )[0] + self.assertEqual(acc_balance.balance, 8500.0) + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_02_accounts_only_with_base_currency_balance(self): + """ + Test Revaluation on Forex account with balance only in base currency + """ + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + pe = get_payment_entry(si.doctype, si.name) + pe.source_exchange_rate = 85 + pe.received_amount = 8500 + pe.save().submit() + + # Cancel the auto created gain/loss JE to simulate balance only in base currency + je = frappe.db.get_all( + "Journal Entry Account", filters={"reference_name": si.name}, pluck="parent" + )[0] + frappe.get_doc("Journal Entry", je).cancel() + + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = (self.company,) + err.posting_date = today() + err.fetch_and_calculate_accounts_data() + err = err.save().submit() + + # Create JV for ERR + self.assertTrue(err.check_journal_entry_condition()) + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) + je = je.submit() + + je.reload() + self.assertEqual(je.voucher_type, "Exchange Gain Or Loss") + self.assertEqual(len(je.accounts), 2) + # Only base currency fields will be posted to + for acc in je.accounts: + self.assertEqual(acc.debit_in_account_currency, 0) + self.assertEqual(acc.credit_in_account_currency, 0) + + self.assertEqual(je.total_debit, 500.0) + self.assertEqual(je.total_credit, 500.0) + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=[ + "sum(debit)-sum(credit) as balance", + "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", + ], + )[0] + # account shouldn't have balance in base and account currency + self.assertEqual(acc_balance.balance, 0.0) + self.assertEqual(acc_balance.balance_in_account_currency, 0.0) + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_03_accounts_only_with_account_currency_balance(self): + """ + Test Revaluation on Forex account with balance only in account currency + """ + precision = frappe.db.get_single_value("System Settings", "currency_precision") + + # posting on previous date to make sure that ERR picks up the Payment entry's exchange + # rate while calculating gain/loss for account currency balance + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=add_days(today(), -1), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 95 + pe.source_exchange_rate = 84.211 + pe.received_amount = 8000 + pe.references = [] + pe.save().submit() + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=[ + "sum(debit)-sum(credit) as balance", + "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", + ], + )[0] + # account should have balance only in account currency + self.assertEqual(flt(acc_balance.balance, precision), 0.0) + self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD + + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = (self.company,) + err.posting_date = today() + err.fetch_and_calculate_accounts_data() + err.set_total_gain_loss() + err = err.save().submit() + + # Create JV for ERR + self.assertTrue(err.check_journal_entry_condition()) + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) + je = je.submit() + + je.reload() + self.assertEqual(je.voucher_type, "Exchange Gain Or Loss") + self.assertEqual(len(je.accounts), 2) + # Only account currency fields will be posted to + for acc in je.accounts: + self.assertEqual(flt(acc.debit, precision), 0.0) + self.assertEqual(flt(acc.credit, precision), 0.0) + + row = [x for x in je.accounts if x.account == self.debtors_usd][0] + self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD + row = [x for x in je.accounts if x.account != self.debtors_usd][0] + self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR + + # total_debit and total_credit will be 0.0, as JV is posting only to account currency fields + self.assertEqual(flt(je.total_debit, precision), 0.0) + self.assertEqual(flt(je.total_credit, precision), 0.0) + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=[ + "sum(debit)-sum(credit) as balance", + "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", + ], + )[0] + # account shouldn't have balance in base and account currency post revaluation + self.assertEqual(flt(acc_balance.balance, precision), 0.0) + self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0) + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_04_get_account_details_function(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import ( + get_account_details, + ) + + account_details = get_account_details( + self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05 + ) + # not checking for new exchange rate and balances as it is dependent on live exchange rates + expected_data = { + "account_currency": "USD", + "balance_in_base_currency": 8000.0, + "balance_in_account_currency": 100.0, + "current_exchange_rate": 80.0, + "zero_balance": False, + "new_balance_in_account_currency": 100.0, + } + + for key, val in expected_data.items(): + self.assertEqual(expected_data.get(key), account_details.get(key)) From ffcbcd7397626c13e9d72120e73e325e0cba8c06 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 27 Aug 2023 23:35:59 +0530 Subject: [PATCH 46/60] test: add test case for SCR scrap items --- .../test_subcontracting_receipt.py | 73 ++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index a170527e2d..0c163916b3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -23,6 +23,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( make_subcontracted_items, set_backflush_based_on, ) +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -507,8 +508,6 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) def test_subcontracting_receipt_raw_material_rate(self): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - # Step - 1: Set Backflush Based On as "BOM" set_backflush_based_on("BOM") @@ -625,6 +624,76 @@ class TestSubcontractingReceipt(FrappeTestCase): # ValidationError should not be raised as `Inspection Required before Purchase` is disabled scr2.submit() + def test_scrap_items_for_subcontracting_receipt(self): + set_backflush_based_on("BOM") + + fg_item = "Subcontracted Item SA1" + + # Create Raw Materials + raw_materials = [ + make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name, + make_item(properties={"is_stock_item": 1, "valuation_rate": 200}).name, + ] + + # Create Scrap Items + scrap_item_1 = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name + scrap_item_2 = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name + scrap_items = [scrap_item_1, scrap_item_2] + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 10, + }, + ] + + # Create BOM with Scrap Items + bom = make_bom( + item=fg_item, raw_materials=raw_materials, rate=100, currency="INR", do_not_submit=True + ) + for idx, item in enumerate(bom.items): + item.qty = 1 * (idx + 1) + for idx, item in enumerate(scrap_items): + bom.append( + "scrap_items", + { + "item_code": item, + "stock_qty": 1 * (idx + 1), + "rate": 10 * (idx + 1), + }, + ) + bom.save() + bom.submit() + + # Create PO and SCO + sco = get_subcontracting_order(service_items=service_items) + + # Inward Raw Materials + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + # Transfer RM's to Subcontractor + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + # Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.save() + + # Test - 1: Scrap Items should be fetched from BOM in items table with `is_scrap_item` = 1 + scr_scrap_items = set([item.item_code for item in scr.items if item.is_scrap_item]) + self.assertEqual(len(scr.items), 3) # 1 FG Item + 2 Scrap Items + self.assertEqual(scr_scrap_items, set(scrap_items)) + + scr.submit() + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) From 91927f2adac40844d39a9ee06cc5fec8424dcdfd Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 28 Aug 2023 00:21:51 +0530 Subject: [PATCH 47/60] chore: woocommerce linter issue (#36837) --- .../erpnext_integrations/connectors/woocommerce_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index b140df2068..2b2da7b971 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -43,7 +43,7 @@ def _order(*args, **kwargs): event = "created" # Ignore the test ping issued during WooCommerce webhook configuration # Ref: https://github.com/woocommerce/woocommerce/issues/15642 - if frappe.request.data.decode('utf-8').startswith('webhook_id='): + if frappe.request.data.decode("utf-8").startswith("webhook_id="): return "success" elif frappe.request and frappe.request.data: verify_request() From 714b8289c1cf23d9a0039e94f305d926a82a28a0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 13:59:00 +0530 Subject: [PATCH 48/60] fix: fetch rounded total while pulling reference details on SO --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 032aa80ff2..9ed3d32c57 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1993,10 +1993,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre if not total_amount: if party_account_currency == company_currency: # for handling cases that don't have multi-currency (base field) - total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total") + total_amount = ( + ref_doc.get("base_rounded_total") + or ref_doc.get("rounded_total") + or ref_doc.get("base_grand_total") + or ref_doc.get("grand_total") + ) exchange_rate = 1 else: - total_amount = ref_doc.get("grand_total") + total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total") if not exchange_rate: # Get the exchange rate from the original ref doc # or get it based on the posting date of the ref doc. From 67a0969b782deb1847ea75e2f565df824d6df36e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 14:21:21 +0530 Subject: [PATCH 49/60] test: allocation err misfire on Sales Order --- .../doctype/payment_entry/test_payment_entry.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index c8bf6644a5..4a3cb30976 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1244,6 +1244,22 @@ class TestPaymentEntry(FrappeTestCase): template.allocate_payment_based_on_payment_terms = 1 template.save() + def test_allocation_validation_for_sales_order(self): + so = make_sales_order(do_not_save=True) + so.items[0].rate = 99.55 + so.save().submit() + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") + pe.paid_from = "Debtors - _TC" + pe.paid_amount = 45.55 + pe.references[0].allocated_amount = 45.55 + pe.save().submit() + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") + pe.paid_from = "Debtors - _TC" + pe.save().submit() + + so.reload() + self.assertEqual(so.advance_paid, so.rounded_total) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From 2fdbe82835625b578b052ac09a5a4f531e2beab5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 14:30:37 +0530 Subject: [PATCH 50/60] test: assert rounded amount is calculated --- erpnext/accounts/doctype/payment_entry/test_payment_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 4a3cb30976..edfec41918 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1248,6 +1248,7 @@ class TestPaymentEntry(FrappeTestCase): so = make_sales_order(do_not_save=True) so.items[0].rate = 99.55 so.save().submit() + self.assertGreater(so.rounded_total, 0.0) pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") pe.paid_from = "Debtors - _TC" pe.paid_amount = 45.55 @@ -1255,6 +1256,7 @@ class TestPaymentEntry(FrappeTestCase): pe.save().submit() pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") pe.paid_from = "Debtors - _TC" + # No validation error should be thrown here. pe.save().submit() so.reload() From 592c7b5ff123f99376cfc87915e9773da7dc9b7c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 28 Aug 2023 00:08:05 +0530 Subject: [PATCH 51/60] fix: test cases --- .../doctype/subcontracting_order/subcontracting_order.py | 5 ++++- .../subcontracting_order/test_subcontracting_order.py | 7 +++++++ .../subcontracting_receipt/subcontracting_receipt.py | 2 +- .../subcontracting_receipt/test_subcontracting_receipt.py | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 0b14d4d9f5..b7b344584c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -203,7 +203,10 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None): { "Subcontracting Order": { "doctype": "Subcontracting Receipt", - "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "field_map": { + "supplier_warehouse": "supplier_warehouse", + "set_warehouse": "set_warehouse", + }, "validation": { "docstatus": ["=", 1], }, diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 6a2983faaa..22fdc13cc1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -591,6 +591,13 @@ def create_subcontracting_order(**args): for idx, val in enumerate(sco.items): val.warehouse = warehouses[idx] + warehouses = set() + for item in sco.items: + warehouses.add(item.warehouse) + + if len(warehouses) == 1: + sco.set_warehouse = list(warehouses)[0] + if not args.do_not_save: sco.insert() if not args.do_not_submit: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index c601ddb999..c0a567d7a6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -55,7 +55,7 @@ class SubcontractingReceipt(SubcontractingController): super(SubcontractingReceipt, self).validate() - if self.is_new() and self.get("_action") == "save": + if self.is_new() and self.get("_action") == "save" and not frappe.flags.in_test: self.get_scrap_items() self.set_missing_values() diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 0c163916b3..1828f6960f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -686,6 +686,7 @@ class TestSubcontractingReceipt(FrappeTestCase): # Create Subcontracting Receipt scr = make_subcontracting_receipt(sco.name) scr.save() + scr.get_scrap_items() # Test - 1: Scrap Items should be fetched from BOM in items table with `is_scrap_item` = 1 scr_scrap_items = set([item.item_code for item in scr.items if item.is_scrap_item]) From 9d330a13edb7728e93327a10f28f73f1ae061bad Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 28 Aug 2023 16:09:05 +0530 Subject: [PATCH 52/60] fix: get `Valuation Rate` instead of BOM rate --- .../subcontracting_receipt.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index c0a567d7a6..8a12e3bcd0 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -8,6 +8,7 @@ from frappe.utils import cint, flt, getdate, nowdate import erpnext from erpnext.accounts.utils import get_account_currency from erpnext.controllers.subcontracting_controller import SubcontractingController +from erpnext.stock.stock_ledger import get_valuation_rate class SubcontractingReceipt(SubcontractingController): @@ -159,6 +160,17 @@ class SubcontractingReceipt(SubcontractingController): bom = frappe.get_doc("BOM", item.bom) for scrap_item in bom.scrap_items: qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity)) + rate = ( + get_valuation_rate( + scrap_item.item_code, + self.set_warehouse, + self.doctype, + self.name, + currency=erpnext.get_company_currency(self.company), + company=self.company, + ) + or scrap_item.rate + ) self.append( "items", { @@ -169,12 +181,12 @@ class SubcontractingReceipt(SubcontractingController): "qty": qty, "stock_uom": scrap_item.stock_uom, "recalculate_rate": 0, - "rate": scrap_item.rate, + "rate": rate, "rm_cost_per_qty": 0, "service_cost_per_qty": 0, "additional_cost_per_qty": 0, "scrap_cost_per_qty": 0, - "amount": qty * scrap_item.rate, + "amount": qty * rate, "warehouse": self.set_warehouse, "rejected_warehouse": self.rejected_warehouse, }, From 3a2933db4d12766d3183893ba2edc9cb5faca527 Mon Sep 17 00:00:00 2001 From: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:14:52 +0530 Subject: [PATCH 53/60] fix: error in report when data is not available to load chart in report (#36842) --- .../assets/report/fixed_asset_register/fixed_asset_register.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index bf62a8fb39..383be97347 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -156,6 +156,8 @@ def get_data(filters): def prepare_chart_data(data, filters): + if not data: + return labels_values_map = {} if filters.filter_based_on not in ("Date Range", "Fiscal Year"): filters_filter_based_on = "Date Range" From 0d22fe02be6c35efb0378858c9a33b8ec05d0b5f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 28 Aug 2023 22:42:14 +0530 Subject: [PATCH 54/60] refactor!: Lower all perm to `Desk User` --- erpnext/crm/doctype/lead/lead.json | 6 +- erpnext/projects/doctype/project/project.json | 4 +- .../quality_action/quality_action.json | 5 +- .../quality_feedback/quality_feedback.json | 5 +- .../doctype/quality_goal/quality_goal.json | 5 +- .../quality_meeting/quality_meeting.json | 7 +- .../quality_procedure/quality_procedure.json | 5 +- .../quality_review/quality_review.json | 5 +- .../setup/doctype/item_group/item_group.json | 5 +- .../doctype/print_heading/print_heading.json | 175 ++++++------------ erpnext/stock/doctype/item/item.json | 4 +- .../service_level_agreement.json | 4 +- 12 files changed, 86 insertions(+), 144 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 0cb8824577..dafbd9f06d 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -516,7 +516,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2023-04-14 18:20:05.044791", + "modified": "2023-08-28 22:28:00.104413", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -527,7 +527,7 @@ "permlevel": 1, "read": 1, "report": 1, - "role": "All" + "role": "Desk User" }, { "create": 1, @@ -583,4 +583,4 @@ "states": [], "subject_field": "title", "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 502ee57415..715b09c64b 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -453,7 +453,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2023-06-28 18:57:11.603497", + "modified": "2023-08-28 22:27:28.370849", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -475,7 +475,7 @@ "permlevel": 1, "read": 1, "report": 1, - "role": "All" + "role": "Desk User" }, { "create": 1, diff --git a/erpnext/quality_management/doctype/quality_action/quality_action.json b/erpnext/quality_management/doctype/quality_action/quality_action.json index 0cc2a98cd2..f0b33b9eaf 100644 --- a/erpnext/quality_management/doctype/quality_action/quality_action.json +++ b/erpnext/quality_management/doctype/quality_action/quality_action.json @@ -91,7 +91,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-27 16:21:59.533937", + "modified": "2023-08-28 22:33:14.358143", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Action", @@ -117,12 +117,13 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "Desk User", "share": 1, "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json index f3bd0ddb2e..5fe6375fcc 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json @@ -61,7 +61,7 @@ "link_fieldname": "feedback" } ], - "modified": "2020-10-27 16:20:10.918544", + "modified": "2023-08-28 22:21:36.144820", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Feedback", @@ -87,12 +87,13 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "Desk User", "share": 1, "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_goal/quality_goal.json b/erpnext/quality_management/doctype/quality_goal/quality_goal.json index 26802550dc..f2b6ebc589 100644 --- a/erpnext/quality_management/doctype/quality_goal/quality_goal.json +++ b/erpnext/quality_management/doctype/quality_goal/quality_goal.json @@ -76,7 +76,7 @@ "link_fieldname": "goal" } ], - "modified": "2020-10-27 15:57:59.368605", + "modified": "2023-08-28 22:33:27.718899", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Goal", @@ -102,12 +102,13 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "Desk User", "share": 1, "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json index e2125c3933..7ab28d8443 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json @@ -48,7 +48,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-27 16:36:45.657883", + "modified": "2023-08-28 22:33:57.447634", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Meeting", @@ -74,7 +74,7 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "Desk User", "share": 1, "write": 1 } @@ -82,5 +82,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json index f588f9aea1..176669ffbc 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -115,7 +115,7 @@ "link_fieldname": "procedure" } ], - "modified": "2020-10-26 15:25:39.316088", + "modified": "2023-08-28 22:33:36.483420", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure", @@ -142,12 +142,13 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "Desk User", "share": 1, "write": 1 } ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.json b/erpnext/quality_management/doctype/quality_review/quality_review.json index 31ad341362..f38e8a50d6 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review.json +++ b/erpnext/quality_management/doctype/quality_review/quality_review.json @@ -84,7 +84,7 @@ "link_fieldname": "review" } ], - "modified": "2020-10-21 12:56:47.046172", + "modified": "2023-08-28 22:33:22.472980", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Review", @@ -110,7 +110,7 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "role": "Desk User", "share": 1, "write": 1 }, @@ -129,6 +129,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "goal", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 2986087277..e0f5090474 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -233,7 +233,7 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2023-01-05 12:21:30.458628", + "modified": "2023-08-28 22:27:48.382985", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", @@ -266,7 +266,6 @@ "read": 1, "report": 1, "role": "Item Manager", - "set_user_permissions": 1, "share": 1, "write": 1 }, @@ -296,7 +295,7 @@ "export": 1, "print": 1, "report": 1, - "role": "All", + "role": "Desk User", "select": 1, "share": 1 } diff --git a/erpnext/setup/doctype/print_heading/print_heading.json b/erpnext/setup/doctype/print_heading/print_heading.json index dc07f0c8d8..1083583037 100644 --- a/erpnext/setup/doctype/print_heading/print_heading.json +++ b/erpnext/setup/doctype/print_heading/print_heading.json @@ -1,131 +1,68 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:print_heading", - "beta": 0, - "creation": "2013-01-10 16:34:24", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:print_heading", + "creation": "2013-01-10 16:34:24", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "print_heading", + "description" + ], "fields": [ { - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "fieldname": "print_heading", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "label": "Print Heading", - "length": 0, - "no_copy": 0, - "oldfieldname": "print_heading", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "fieldname": "print_heading", + "fieldtype": "Data", + "in_filter": 1, + "in_list_view": 1, + "label": "Print Heading", + "oldfieldname": "print_heading", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Small Text", "width": "300px" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-font", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-07-25 05:24:25.628101", - "modified_by": "Administrator", - "module": "Setup", - "name": "Print Heading", - "owner": "Administrator", + ], + "icon": "fa fa-font", + "idx": 1, + "links": [], + "modified": "2023-08-28 22:17:42.041255", + "modified_by": "Administrator", + "module": "Setup", + "name": "Print Heading", + "naming_rule": "By fieldname", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "All", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Desk User" } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "print_heading", - "track_seen": 0 + ], + "quick_entry": 1, + "search_fields": "print_heading", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 87c2a7ea69..756d0040f1 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -912,7 +912,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-07-14 17:18:18.658942", + "modified": "2023-08-28 22:16:40.305094", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -971,7 +971,7 @@ "export": 1, "print": 1, "report": 1, - "role": "All", + "role": "Desk User", "select": 1, "share": 1 } diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 1c6f24b23c..5882033075 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -192,7 +192,7 @@ } ], "links": [], - "modified": "2023-04-21 17:16:56.192560", + "modified": "2023-08-28 22:17:54.740924", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", @@ -213,7 +213,7 @@ }, { "read": 1, - "role": "All" + "role": "Desk User" } ], "sort_field": "modified", From 26e8b8f95920cdc5b8d541a394a3389c0480a8fa Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Tue, 29 Aug 2023 09:03:31 +0530 Subject: [PATCH 55/60] fix: create entries for only PR items present in LCV (#36852) * fix: check if item code exists in lcv before creating gle * refactor: use qb to fetch lcv items --- .../purchase_invoice/purchase_invoice.py | 29 ++++++------- .../landed_cost_voucher.py | 42 +++++++++++++------ .../purchase_receipt/purchase_receipt.py | 41 +++++++++--------- 3 files changed, 65 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 9f1224d65e..be19bca1fd 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -759,21 +759,22 @@ class PurchaseInvoice(BuyingController): # Amount added through landed-cost-voucher if landed_cost_entries: - for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): - gl_entries.append( - self.get_gl_dict( - { - "account": account, - "against": item.expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount["base_amount"]), - "credit_in_account_currency": flt(amount["amount"]), - "project": item.project or self.project, - }, - item=item, + if (item.item_code, item.name) in landed_cost_entries: + for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": item.expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), + "project": item.project or self.project, + }, + item=item, + ) ) - ) # sub-contracting warehouse if flt(item.rm_supp_cost): diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 111a0861b7..7f0dc2df9f 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.meta import get_field_precision +from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt import erpnext @@ -19,19 +20,7 @@ class LandedCostVoucher(Document): self.set("items", []) for pr in self.get("purchase_receipts"): if pr.receipt_document_type and pr.receipt_document: - pr_items = frappe.db.sql( - """select pr_item.item_code, pr_item.description, - pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name, - pr_item.cost_center, pr_item.is_fixed_asset - from `tab{doctype} Item` pr_item where parent = %s - and exists(select name from tabItem - where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1)) - """.format( - doctype=pr.receipt_document_type - ), - pr.receipt_document, - as_dict=True, - ) + pr_items = get_pr_items(pr) for d in pr_items: item = self.append("items") @@ -247,3 +236,30 @@ class LandedCostVoucher(Document): ), tuple([item.valuation_rate] + serial_nos), ) + + +def get_pr_items(purchase_receipt): + item = frappe.qb.DocType("Item") + pr_item = frappe.qb.DocType(purchase_receipt.receipt_document_type + " Item") + return ( + frappe.qb.from_(pr_item) + .inner_join(item) + .on(item.name == pr_item.item_code) + .select( + pr_item.item_code, + pr_item.description, + pr_item.qty, + pr_item.base_rate, + pr_item.base_amount, + pr_item.name, + pr_item.cost_center, + pr_item.is_fixed_asset, + ConstantColumn(purchase_receipt.receipt_document_type).as_("receipt_document_type"), + ConstantColumn(purchase_receipt.receipt_document).as_("receipt_document"), + ) + .where( + (pr_item.parent == purchase_receipt.receipt_document) + & ((item.is_stock_item == 1) | (item.is_fixed_asset == 1)) + ) + .run(as_dict=True) + ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0b5dc05c3a..60aefddf4c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -470,27 +470,28 @@ class PurchaseReceipt(BuyingController): # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: - for account, amount in landed_cost_entries[(d.item_code, d.name)].items(): - account_currency = get_account_currency(account) - credit_amount = ( - flt(amount["base_amount"]) - if (amount["base_amount"] or account_currency != self.company_currency) - else flt(amount["amount"]) - ) + if (d.item_code, d.name) in landed_cost_entries: + for account, amount in landed_cost_entries[(d.item_code, d.name)].items(): + account_currency = get_account_currency(account) + credit_amount = ( + flt(amount["base_amount"]) + if (amount["base_amount"] or account_currency != self.company_currency) + else flt(amount["amount"]) + ) - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=0.0, - credit=credit_amount, - remarks=remarks, - against_account=warehouse_account_name, - credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, - project=d.project, - item=d, - ) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=d.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=warehouse_account_name, + credit_in_account_currency=flt(amount["amount"]), + account_currency=account_currency, + project=d.project, + item=d, + ) if d.rate_difference_with_purchase_invoice and stock_rbnb: account_currency = get_account_currency(stock_rbnb) From 1c237f455e81cc11bda798e2c67fd4fd98369187 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Aug 2023 10:50:12 +0530 Subject: [PATCH 56/60] fix(demo): COA fixes for Mexico country (#36840) --- .../verified/mx_plan_de_cuentas.json | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/mx_plan_de_cuentas.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/mx_plan_de_cuentas.json index e98c2d6d38..858f05c442 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/mx_plan_de_cuentas.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/mx_plan_de_cuentas.json @@ -109,8 +109,7 @@ } }, "INVENTARIOS": { - "account_type": "Stock", - "is_group": 1 + "account_type": "Stock" } }, "ACTIVO LARGO PLAZO": { @@ -398,10 +397,18 @@ "INGRESOS POR SERVICIOS 1": {} }, "VENTAS": { - "VENTAS EXPORTACION": {}, - "VENTAS INMUEBLES": {}, - "VENTAS NACIONALES": {}, - "VENTAS NACIONALES AL DETAL": {} + "VENTAS EXPORTACION": { + "account_type": "Income Account" + }, + "VENTAS INMUEBLES": { + "account_type": "Income Account" + }, + "VENTAS NACIONALES": { + "account_type": "Income Account" + }, + "VENTAS NACIONALES AL DETAL": { + "account_type": "Income Account" + } } } }, From 5c4df3e0e3e76116288e022db5b2cdb85d028086 Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Tue, 29 Aug 2023 07:29:58 +0200 Subject: [PATCH 57/60] fix: Setup flake8 to stop turning trailing commas into tuples (#36850) --- .pre-commit-config.yaml | 1 + .../test_exchange_rate_revaluation.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c9a60c7c4..30be903ae8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,7 @@ repos: - id: flake8 additional_dependencies: [ 'flake8-bugbear', + 'flake8-tuple', ] args: ['--config', '.github/helper/.flake8_strict'] exclude: ".*setup.py$" diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index ced04ced3f..e520872122 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -67,7 +67,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): si.save().submit() err = frappe.new_doc("Exchange Rate Revaluation") - err.company = (self.company,) + err.company = self.company err.posting_date = today() accounts = err.get_accounts_data() err.extend("accounts", accounts) @@ -133,7 +133,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): frappe.get_doc("Journal Entry", je).cancel() err = frappe.new_doc("Exchange Rate Revaluation") - err.company = (self.company,) + err.company = self.company err.posting_date = today() err.fetch_and_calculate_accounts_data() err = err.save().submit() @@ -215,7 +215,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD err = frappe.new_doc("Exchange Rate Revaluation") - err.company = (self.company,) + err.company = self.company err.posting_date = today() err.fetch_and_calculate_accounts_data() err.set_total_gain_loss() From 4856e115490c298fd15c434aadcf1d525895bf6c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 29 Aug 2023 15:51:04 +0530 Subject: [PATCH 58/60] chore: update integrations workspace --- .../erpnext_integrations.json | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json index 5c4be6ffaa..510317f5c2 100644 --- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json +++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"e88ADOJ7WC\",\"type\":\"header\",\"data\":{\"text\":\"Integrations\",\"col\":12}},{\"id\":\"G0tyx9WOfm\",\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"id\":\"nu4oSjH5Rd\",\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"id\":\"nG8cdkpzoc\",\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"id\":\"4hwuQn6E95\",\"type\":\"card\",\"data\":{\"card_name\":\"Communication Channels\",\"col\":4}},{\"id\":\"sEGAzTJRmq\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"ZC6xu-cLBR\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"e88ADOJ7WC\",\"type\":\"header\",\"data\":{\"text\":\"Integrations\",\"col\":12}},{\"id\":\"pZEYOOCdB0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Browse Apps\",\"col\":3}},{\"id\":\"St7AHbhVOr\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"nu4oSjH5Rd\",\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"id\":\"G0tyx9WOfm\",\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"id\":\"nG8cdkpzoc\",\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"id\":\"4hwuQn6E95\",\"type\":\"card\",\"data\":{\"card_name\":\"Communication Channels\",\"col\":4}},{\"id\":\"sEGAzTJRmq\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}}]", "creation": "2020-08-20 19:30:48.138801", "custom_blocks": [], "docstatus": 0, @@ -221,27 +221,9 @@ "link_type": "DocType", "onboard": 0, "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Settings", - "link_count": 2, - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Woocommerce Settings", - "link_count": 0, - "link_to": "Woocommerce Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" } ], - "modified": "2023-05-24 14:47:26.984717", + "modified": "2023-08-29 15:48:59.010704", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "ERPNext Integrations", @@ -253,6 +235,14 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 21.0, - "shortcuts": [], + "shortcuts": [ + { + "color": "Grey", + "doc_view": "List", + "label": "Browse Apps", + "type": "URL", + "url": "https://frappecloud.com/marketplace" + } + ], "title": "ERPNext Integrations" } \ No newline at end of file From dea802dc4134f7ed1f85ff337ddcb2d521087644 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 29 Aug 2023 16:53:00 +0530 Subject: [PATCH 59/60] fix: added valuation field type (Float/Currency) in the filter (#36866) --- erpnext/stock/report/stock_balance/stock_balance.js | 8 ++++++++ erpnext/stock/report/stock_balance/stock_balance.py | 5 ++++- erpnext/stock/report/stock_ledger/stock_ledger.js | 10 +++++++++- erpnext/stock/report/stock_ledger/stock_ledger.py | 12 ++++++++---- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 33ed955a5c..6de5f00ece 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -71,6 +71,14 @@ frappe.query_reports["Stock Balance"] = { "width": "80", "options": "Warehouse Type" }, + { + "fieldname": "valuation_field_type", + "label": __("Valuation Field Type"), + "fieldtype": "Select", + "width": "80", + "options": "Currency\nFloat", + "default": "Currency" + }, { "fieldname":"include_uom", "label": __("Include UOM"), diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index d60e9b57ab..1dafb4d8dd 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -446,9 +446,12 @@ class StockBalanceReport(object): { "label": _("Valuation Rate"), "fieldname": "val_rate", - "fieldtype": "Float", + "fieldtype": self.filters.valuation_field_type or "Currency", "width": 90, "convertible": "rate", + "options": "Company:company:default_currency" + if self.filters.valuation_field_type == "Currency" + else None, }, { "label": _("Reserved Stock"), diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 0def161d28..b00b422a67 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -82,7 +82,15 @@ frappe.query_reports["Stock Ledger"] = { "label": __("Include UOM"), "fieldtype": "Link", "options": "UOM" - } + }, + { + "fieldname": "valuation_field_type", + "label": __("Valuation Field Type"), + "fieldtype": "Select", + "width": "80", + "options": "Currency\nFloat", + "default": "Currency" + }, ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index ed28ed3ee4..eeef39641b 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -196,17 +196,21 @@ def get_columns(filters): { "label": _("Avg Rate (Balance Stock)"), "fieldname": "valuation_rate", - "fieldtype": "Float", + "fieldtype": filters.valuation_field_type, "width": 180, - "options": "Company:company:default_currency", + "options": "Company:company:default_currency" + if filters.valuation_field_type == "Currency" + else None, "convertible": "rate", }, { "label": _("Valuation Rate"), "fieldname": "in_out_rate", - "fieldtype": "Float", + "fieldtype": filters.valuation_field_type, "width": 140, - "options": "Company:company:default_currency", + "options": "Company:company:default_currency" + if filters.valuation_field_type == "Currency" + else None, "convertible": "rate", }, { From 92e503f75be60d45436a7da84a3affd668cf1683 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Aug 2023 20:45:57 +0530 Subject: [PATCH 60/60] fix: Sales/Purchase register showing duplicate records --- .../purchase_register/purchase_register.py | 23 ++++++++++--- .../report/sales_register/sales_register.py | 28 +++++++++++----- erpnext/accounts/report/utils.py | 33 ++++++++++--------- 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index c7b7e2f7c1..ca8b9f0842 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -10,8 +10,8 @@ from pypika import Order from erpnext.accounts.party import get_party_account from erpnext.accounts.report.utils import ( + apply_common_conditions, get_advance_taxes_and_charges, - get_conditions, get_journal_entries, get_opening_row, get_party_details, @@ -378,11 +378,8 @@ def get_account_columns(invoice_list, include_payments): def get_invoices(filters, additional_query_columns): pi = frappe.qb.DocType("Purchase Invoice") - invoice_item = frappe.qb.DocType("Purchase Invoice Item") query = ( frappe.qb.from_(pi) - .inner_join(invoice_item) - .on(pi.name == invoice_item.parent) .select( ConstantColumn("Purchase Invoice").as_("doctype"), pi.name, @@ -402,23 +399,39 @@ def get_invoices(filters, additional_query_columns): .where((pi.docstatus == 1)) .orderby(pi.posting_date, pi.name, order=Order.desc) ) + if additional_query_columns: for col in additional_query_columns: query = query.select(col) + if filters.get("supplier"): query = query.where(pi.supplier == filters.supplier) - query = get_conditions( + + query = get_conditions(filters, query, "Purchase Invoice") + + query = apply_common_conditions( filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item" ) + if filters.get("include_payments"): party_account = get_party_account( "Supplier", filters.get("supplier"), filters.get("company"), include_advance=True ) query = query.where(pi.credit_to.isin(party_account)) + invoices = query.run(as_dict=True) return invoices +def get_conditions(filters, query, doctype): + parent_doc = frappe.qb.DocType(doctype) + + if filters.get("mode_of_payment"): + query = query.where(parent_doc.mode_of_payment == filters.mode_of_payment) + + return query + + def get_payments(filters): args = frappe._dict( account="credit_to", diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 35d8d16479..d3fc373825 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -11,8 +11,8 @@ from pypika import Order from erpnext.accounts.party import get_party_account from erpnext.accounts.report.utils import ( + apply_common_conditions, get_advance_taxes_and_charges, - get_conditions, get_journal_entries, get_opening_row, get_party_details, @@ -415,14 +415,8 @@ def get_account_columns(invoice_list, include_payments): def get_invoices(filters, additional_query_columns): si = frappe.qb.DocType("Sales Invoice") - invoice_item = frappe.qb.DocType("Sales Invoice Item") - invoice_payment = frappe.qb.DocType("Sales Invoice Payment") query = ( frappe.qb.from_(si) - .inner_join(invoice_item) - .on(si.name == invoice_item.parent) - .left_join(invoice_payment) - .on(si.name == invoice_payment.parent) .select( ConstantColumn("Sales Invoice").as_("doctype"), si.name, @@ -447,18 +441,36 @@ def get_invoices(filters, additional_query_columns): .where((si.docstatus == 1)) .orderby(si.posting_date, si.name, order=Order.desc) ) + if additional_query_columns: for col in additional_query_columns: query = query.select(col) + if filters.get("customer"): query = query.where(si.customer == filters.customer) - query = get_conditions( + + query = get_conditions(filters, query, "Sales Invoice") + query = apply_common_conditions( filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item" ) + invoices = query.run(as_dict=True) return invoices +def get_conditions(filters, query, doctype): + parent_doc = frappe.qb.DocType(doctype) + if filters.get("owner"): + query = query.where(parent_doc.owner == filters.owner) + + if filters.get("mode_of_payment"): + payment_doc = frappe.qb.DocType("Sales Invoice Payment") + query = query.inner_join(payment_doc).on(parent_doc.name == payment_doc.parent) + query = query.where(payment_doc.mode_of_payment == filters.mode_of_payment).distinct() + + return query + + def get_payments(filters): args = frappe._dict( account="debit_to", diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 0753fff834..9f96449ba7 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -256,7 +256,8 @@ def get_journal_entries(filters, args): ) .orderby(je.posting_date, je.name, order=Order.desc) ) - query = get_conditions(filters, query, doctype="Journal Entry", payments=True) + query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True) + journal_entries = query.run(as_dict=True) return journal_entries @@ -284,28 +285,17 @@ def get_payment_entries(filters, args): ) .orderby(pe.posting_date, pe.name, order=Order.desc) ) - query = get_conditions(filters, query, doctype="Payment Entry", payments=True) + query = apply_common_conditions(filters, query, doctype="Payment Entry", payments=True) payment_entries = query.run(as_dict=True) return payment_entries -def get_conditions(filters, query, doctype, child_doctype=None, payments=False): +def apply_common_conditions(filters, query, doctype, child_doctype=None, payments=False): parent_doc = frappe.qb.DocType(doctype) if child_doctype: child_doc = frappe.qb.DocType(child_doctype) - if parent_doc.get_table_name() == "tabSales Invoice": - if filters.get("owner"): - query = query.where(parent_doc.owner == filters.owner) - if filters.get("mode_of_payment"): - payment_doc = frappe.qb.DocType("Sales Invoice Payment") - query = query.where(payment_doc.mode_of_payment == filters.mode_of_payment) - if not payments: - if filters.get("brand"): - query = query.where(child_doc.brand == filters.brand) - else: - if filters.get("mode_of_payment"): - query = query.where(parent_doc.mode_of_payment == filters.mode_of_payment) + join_required = False if filters.get("company"): query = query.where(parent_doc.company == filters.company) @@ -320,13 +310,26 @@ def get_conditions(filters, query, doctype, child_doctype=None, payments=False): else: if filters.get("cost_center"): query = query.where(child_doc.cost_center == filters.cost_center) + join_required = True if filters.get("warehouse"): query = query.where(child_doc.warehouse == filters.warehouse) + join_required = True if filters.get("item_group"): query = query.where(child_doc.item_group == filters.item_group) + join_required = True + + if not payments: + if filters.get("brand"): + query = query.where(child_doc.brand == filters.brand) + join_required = True + + if join_required: + query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent) + query = query.distinct() if parent_doc.get_table_name() != "tabJournal Entry": query = filter_invoices_based_on_dimensions(filters, query, parent_doc) + return query