From f68205468c9443a061e66fd0166a28923dd07a2d Mon Sep 17 00:00:00 2001 From: m1ngaa <77512195+m1ngaa@users.noreply.github.com> Date: Wed, 14 Apr 2021 05:50:31 +0800 Subject: [PATCH 001/550] Delete accounts (an empty file) This file has no purpose, and must've been included as a tiny mistake..? --- erpnext/accounts/accounts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 erpnext/accounts/accounts diff --git a/erpnext/accounts/accounts b/erpnext/accounts/accounts deleted file mode 100644 index e69de29bb2..0000000000 From e379f083bbc5b40faa12c8adf41aedccb8b56f48 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Apr 2021 23:04:39 +0530 Subject: [PATCH 002/550] feat(India): Separate Input and Output GST tax accounts --- .../setup_wizard/data/country_wise_tax.json | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 5876488033..9db2122be2 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -881,35 +881,70 @@ ] } ], - "*": [ + "sales_tax_templates": [ { - "title": "In State GST", + "title": "Output GST In-state", "taxes": [ { "account_head": { - "account_name": "SGST", + "account_name": "Output Tax SGST", "tax_rate": 9.00 } }, { "account_head": { - "account_name": "CGST", + "account_name": "Output Tax CGST", "tax_rate": 9.00 } } ] }, { - "title": "Out of State GST", + "title": "Output GST Out-state", "taxes": [ { "account_head": { - "account_name": "IGST", + "account_name": "Output Tax IGST", "tax_rate": 18.00 } } ] + } + ], + "purchase_tax_templates": [ + { + "title": "Input GST In-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax SGST", + "tax_rate": 9.00, + "root_type": "Asset" + } + }, + { + "account_head": { + "account_name": "Input Tax CGST", + "tax_rate": 9.00, + "root_type": "Asset" + } + } + ] }, + { + "title": "Input GST Out-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax IGST", + "tax_rate": 18.00, + "root_type": "Asset" + } + } + ] + } + ], + "*": [ { "title": "VAT 5%", "taxes": [ @@ -1001,7 +1036,7 @@ "Italy VAT 4%":{ "account_name": "IVA 4%", "tax_rate": 4.00 - } + } }, "Ivory Coast": { From 3130ed52ff0625c150b1c0b8492adc4474ddbf60 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Apr 2021 23:23:07 +0530 Subject: [PATCH 003/550] fix: Item Tax templates for GST --- .../setup_wizard/data/country_wise_tax.json | 148 +++++++++++++++++- 1 file changed, 142 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 9db2122be2..3119eeec49 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -820,29 +820,165 @@ "*": { "item_tax_templates": [ { - "title": "In State GST", + "title": "GST 9%", "taxes": [ { "tax_type": { - "account_name": "SGST", + "account_name": "Output Tax SGST", "tax_rate": 9.00 } }, { "tax_type": { - "account_name": "CGST", + "account_name": "Output Tax CGST", "tax_rate": 9.00 } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 18.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 18.00 + } } ] }, { - "title": "Out of State GST", + "title": "GST 5%", "taxes": [ { "tax_type": { - "account_name": "IGST", - "tax_rate": 18.00 + "account_name": "Output Tax SGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Output Tax CGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 5.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 5.0 + } + } + ] + }, + { + "title": "GST 12%", + "taxes": [ + { + "tax_type": { + "account_name": "Output Tax SGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax CGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 12.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 12.0 + } + } + ] + }, + { + "title": "GST 28%", + "taxes": [ + { + "tax_type": { + "account_name": "Output Tax SGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax CGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 28.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 28.0 } } ] From 2acc66db2cc55208c73a045a4f5cd64693d06606 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Apr 2021 11:49:02 +0530 Subject: [PATCH 004/550] fix: Add tax categories on company setup --- .../setup_wizard/data/country_wise_tax.json | 22 +++++++++++++++++++ .../setup_wizard/operations/taxes_setup.py | 10 +++++++++ 2 files changed, 32 insertions(+) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 3119eeec49..e3fde60ef3 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -818,6 +818,28 @@ "India": { "chart_of_accounts": { "*": { + "tax_categories": [ + { + "title": "In-Sate", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Out-Sate", + "is_inter_state": 1, + "gst_state": "" + }, + { + "title": "Reverse Charge", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Registered Composition", + "is_inter_state": 0, + "gst_state": "" + } + ], "item_tax_templates": [ { "title": "GST 9%", diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 429a558c58..578a270dd9 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -78,6 +78,7 @@ def from_detailed_data(company_name, data): sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*') purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*') item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*') + tax_categories = tax_templates.get('tax_categories') if sales_tax_templates: for template in sales_tax_templates: @@ -91,6 +92,10 @@ def from_detailed_data(company_name, data): for template in item_tax_templates: make_item_tax_template(company_name, template) + if tax_categories: + for tax_category in tax_categories: + make_tax_category(tax_category) + def make_taxes_and_charges_template(company_name, doctype, template): template['company'] = company_name @@ -146,6 +151,11 @@ def make_item_tax_template(company_name, template): return frappe.get_doc(template).insert(ignore_permissions=True) +def make_tax_category(tax_category): + """ Make tax category based on title if not already created """ + doctype = 'Tax Category' + if not frappe.db.exists(doctype, tax_category) + frappe.get_doc(tax_category).insert(ignore_permissions=True) def get_or_create_account(company_name, account): """ From 50997709d5f714ed59517177954f0c624a8f7473 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Apr 2021 20:41:57 +0530 Subject: [PATCH 005/550] fix: Update country-wise-tax JSON and tax setup --- .../setup_wizard/data/country_wise_tax.json | 34 ++++++++++++------- .../setup_wizard/operations/taxes_setup.py | 22 ++++++------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index e3fde60ef3..d21ef03e19 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -820,12 +820,12 @@ "*": { "tax_categories": [ { - "title": "In-Sate", + "title": "In-State", "is_inter_state": 0, "gst_state": "" }, { - "title": "Out-Sate", + "title": "Out-State", "is_inter_state": 1, "gst_state": "" }, @@ -1046,16 +1046,19 @@ { "account_head": { "account_name": "Output Tax SGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "account_type": "Tax" } }, { "account_head": { "account_name": "Output Tax CGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "account_type": "Tax" } } - ] + ], + "tax_category": "In-State" }, { "title": "Output GST Out-state", @@ -1063,10 +1066,12 @@ { "account_head": { "account_name": "Output Tax IGST", - "tax_rate": 18.00 + "tax_rate": 18.00, + "account_type": "Tax" } } - ] + ], + "tax_category": "Out-State" } ], "purchase_tax_templates": [ @@ -1077,17 +1082,20 @@ "account_head": { "account_name": "Input Tax SGST", "tax_rate": 9.00, - "root_type": "Asset" + "root_type": "Asset", + "account_type": "Tax" } }, { "account_head": { "account_name": "Input Tax CGST", "tax_rate": 9.00, - "root_type": "Asset" + "root_type": "Asset", + "account_type": "Tax" } } - ] + ], + "tax_category": "In-State" }, { "title": "Input GST Out-state", @@ -1096,10 +1104,12 @@ "account_head": { "account_name": "Input Tax IGST", "tax_rate": 18.00, - "root_type": "Asset" + "root_type": "Asset", + "account_type": "Tax" } } - ] + ], + "tax_category": "Out-State" } ], "*": [ diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 578a270dd9..bbe301bb37 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -80,6 +80,10 @@ def from_detailed_data(company_name, data): item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*') tax_categories = tax_templates.get('tax_categories') + if tax_categories: + for tax_category in tax_categories: + make_tax_category(tax_category) + if sales_tax_templates: for template in sales_tax_templates: make_taxes_and_charges_template(company_name, 'Sales Taxes and Charges Template', template) @@ -92,10 +96,6 @@ def from_detailed_data(company_name, data): for template in item_tax_templates: make_item_tax_template(company_name, template) - if tax_categories: - for tax_category in tax_categories: - make_tax_category(tax_category) - def make_taxes_and_charges_template(company_name, doctype, template): template['company'] = company_name @@ -154,8 +154,9 @@ def make_item_tax_template(company_name, template): def make_tax_category(tax_category): """ Make tax category based on title if not already created """ doctype = 'Tax Category' - if not frappe.db.exists(doctype, tax_category) - frappe.get_doc(tax_category).insert(ignore_permissions=True) + if not frappe.db.exists(doctype, tax_category): + tax_category['doctype'] = doctype + frappe.get_doc(tax_category).insert(ignore_permissions=True) def get_or_create_account(company_name, account): """ @@ -167,12 +168,13 @@ def get_or_create_account(company_name, account): existing_accounts = frappe.get_list('Account', filters={ - 'company': company_name, - 'root_type': root_type + 'account_name': account.get('account_name'), + 'account_number': account.get('account_number', '') }, or_filters={ - 'account_name': account.get('account_name'), - 'account_number': account.get('account_number') + 'company': company_name, + 'root_type': root_type, + 'is_group': 0 } ) From 66a71bdd1a78c4b9253080ed229a8dc25995c53c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Apr 2021 20:42:20 +0530 Subject: [PATCH 006/550] fix: Issues on new company setup --- erpnext/regional/india/setup.py | 10 +++++----- .../report/e_invoice_summary/e_invoice_summary.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 9ded8dab5b..f70ba90506 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -427,13 +427,13 @@ def make_custom_fields(update=True): dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', print_hide=1, hidden=1), - + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', no_copy=1, print_hide=1), - + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', + dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', no_copy=1, print_hide=1), dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', @@ -696,12 +696,12 @@ def set_tax_withholding_category(company): docs = get_tds_details(accounts, fiscal_year) for d in docs: - try: + if not frappe.db.exists("Tax Withholding Category", d.get("name")): doc = frappe.get_doc(d) doc.flags.ignore_permissions = True doc.flags.ignore_mandatory = True doc.insert() - except frappe.DuplicateEntryError: + else: doc = frappe.get_doc("Tax Withholding Category", d.get("name")) if accounts: diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json index 4deb073a53..d0000ad50d 100644 --- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json @@ -11,7 +11,7 @@ "is_standard": "Yes", "json": "{}", "letter_head": "Logo", - "modified": "2021-03-12 12:36:48.689413", + "modified": "2021-03-13 12:36:48.689413", "modified_by": "Administrator", "module": "Regional", "name": "E-Invoice Summary", From 204ea1027f84190b5a26e127b88be19f2945337c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Apr 2021 16:35:52 +0530 Subject: [PATCH 007/550] fix: Ignore validations for Tax Setup --- erpnext/regional/india/setup.py | 10 ++++-- erpnext/setup/doctype/company/company.py | 2 +- .../setup_wizard/operations/taxes_setup.py | 34 ++++++++++++++----- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index f70ba90506..aedd6c88ab 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -25,6 +25,7 @@ def setup_company_independent_fixtures(patch=False): frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) create_gratuity_rule() add_print_formats() + update_accounts_settings_for_taxes() def add_hsn_sac_codes(): # HSN codes @@ -698,6 +699,7 @@ def set_tax_withholding_category(company): for d in docs: if not frappe.db.exists("Tax Withholding Category", d.get("name")): doc = frappe.get_doc(d) + doc.flags.ignore_validate = True doc.flags.ignore_permissions = True doc.flags.ignore_mandatory = True doc.insert() @@ -714,11 +716,12 @@ def set_tax_withholding_category(company): doc.append("rates", d.get('rates')[0]) doc.flags.ignore_permissions = True + doc.flags.ignore_validdate = True doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True doc.save() def set_tds_account(docs, company): - abbr = frappe.get_value("Company", company, "abbr") parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company}) if parent_account: docs.extend([ @@ -877,7 +880,6 @@ def get_tds_details(accounts, fiscal_year): ] def create_gratuity_rule(): - # Standard Indain Gratuity Rule if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"): rule = frappe.new_doc("Gratuity Rule") @@ -895,3 +897,7 @@ def create_gratuity_rule(): rule.flags.ignore_mandatory = True rule.save() + +def update_accounts_settings_for_taxes(): + if frappe.db.count('Company') == 1: + frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 64e027dd28..aa4bc98875 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -110,7 +110,7 @@ class Company(NestedSet): self.create_default_warehouses() if frappe.flags.country_change: - install_country_fixtures(self.name) + install_country_fixtures(self.name, self.country) self.create_default_tax_template() if not frappe.db.get_value("Department", {"company": self.name}): diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index bbe301bb37..a644da9292 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -123,8 +123,11 @@ def make_taxes_and_charges_template(company_name, doctype, template): if fieldname not in tax_row: tax_row[fieldname] = default_value - return frappe.get_doc(template).insert(ignore_permissions=True) - + doc = frappe.get_doc(template) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True) + return doc def make_item_tax_template(company_name, template): """Create an Item Tax Template. @@ -149,14 +152,21 @@ def make_item_tax_template(company_name, template): if 'tax_rate' not in tax_row: tax_row['tax_rate'] = account_data.get('tax_rate') - return frappe.get_doc(template).insert(ignore_permissions=True) + doc = frappe.get_doc(template) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True) + return doc def make_tax_category(tax_category): """ Make tax category based on title if not already created """ doctype = 'Tax Category' if not frappe.db.exists(doctype, tax_category): tax_category['doctype'] = doctype - frappe.get_doc(tax_category).insert(ignore_permissions=True) + doc = frappe.get_doc(tax_category) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True) def get_or_create_account(company_name, account): """ @@ -169,7 +179,8 @@ def get_or_create_account(company_name, account): existing_accounts = frappe.get_list('Account', filters={ 'account_name': account.get('account_name'), - 'account_number': account.get('account_number', '') + 'account_number': account.get('account_number', ''), + 'company': company_name }, or_filters={ 'company': company_name, @@ -191,8 +202,11 @@ def get_or_create_account(company_name, account): account['root_type'] = root_type account['is_group'] = 0 - return frappe.get_doc(account).insert(ignore_permissions=True, ignore_mandatory=True) - + doc = frappe.get_doc(account) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True, ignore_mandatory=True) + return doc def get_or_create_tax_group(company_name, root_type): # Look for a group account of type 'Tax' @@ -237,7 +251,11 @@ def get_or_create_tax_group(company_name, root_type): 'account_type': 'Tax', 'account_name': account_name, 'parent_account': root_account.name - }).insert(ignore_permissions=True) + }) + + tax_group_account.flags.ignore_links = True + tax_group_account.flags.ignore_validate = True + tax_group_account.insert(ignore_permissions=True) tax_group_name = tax_group_account.name From a66184fe3cd9527b8b13c9b4897b46ec0dcbed8c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 12:22:16 +0530 Subject: [PATCH 008/550] fix: Remove redundant get_doc --- erpnext/regional/india/setup.py | 2 +- erpnext/setup/doctype/company/company.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index aedd6c88ab..71b5661a4e 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -646,7 +646,7 @@ def make_custom_fields(update=True): def make_fixtures(company=None): docs = [] - company = company.name if company else frappe.db.get_value("Global Defaults", None, "default_company") + company = company or frappe.db.get_value("Global Defaults", None, "default_company") set_salary_components(docs) set_tds_account(docs, company) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index aa4bc98875..779e976181 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -435,13 +435,12 @@ def get_name_with_abbr(name, company): return " - ".join(parts) -def install_country_fixtures(company): - company_doc = frappe.get_doc("Company", company) - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(company_doc.country)) +def install_country_fixtures(company, country): + path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: - module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(company_doc.country)) - frappe.get_attr(module_name)(company_doc, False) + module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country)) + frappe.get_attr(module_name)(company, False) except Exception as e: frappe.log_error() frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country))) From f6610c96dd6cf3585dcbadfaf90bc973a60bb40f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 12:23:11 +0530 Subject: [PATCH 009/550] fix: Gracefully handle duplicate bank account name to make setup faster --- erpnext/accounts/doctype/account/account.py | 6 +++++ .../chart_of_accounts/chart_of_accounts.py | 18 ------------- erpnext/public/js/setup_wizard.js | 26 ------------------- 3 files changed, 6 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 0606823821..60269943cf 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -28,6 +28,12 @@ class Account(NestedSet): from erpnext.accounts.utils import get_autoname_with_number self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) + def before_insert(self): + # Update Bank account name if conflicting with any other account + if frappe.flags.in_install and self.account_type == 'Bank': + if frappe.db.get_value('Account', {'account_name': self.account_name}): + self.account_name = self.account_name + '-1' + def validate(self): from erpnext.accounts.utils import validate_field_number if frappe.local.flags.allow_unverified_charts: diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 0e3b24cda3..9d3174204b 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -188,24 +188,6 @@ def build_account_tree(tree, parent, all_accounts): # call recursively to build a subtree for current account build_account_tree(tree[child.account_name], child, all_accounts) -@frappe.whitelist() -def validate_bank_account(coa, bank_account): - accounts = [] - chart = get_chart(coa) - - if chart: - def _get_account_names(account_master): - for account_name, child in iteritems(account_master): - if account_name not in ["account_number", "account_type", - "root_type", "is_group", "tax_rate"]: - accounts.append(account_name) - - _get_account_names(child) - - _get_account_names(chart) - - return (bank_account in accounts) - @frappe.whitelist() def build_tree_from_json(chart_template, chart_data=None): ''' get chart template from its folder and parse the json to be rendered as tree ''' diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index ef03b01698..a3045724fe 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -139,36 +139,10 @@ erpnext.setup.slides_settings = [ }, validate: function () { - let me = this; - let exist; - if (!this.validate_fy_dates()) { return false; } - // Validate bank name - if(me.values.bank_account){ - frappe.call({ - async: false, - method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", - args: { - "coa": me.values.chart_of_accounts, - "bank_account": me.values.bank_account - }, - callback: function (r) { - if(r.message){ - exist = r.message; - me.get_field("bank_account").set_value(""); - let message = __('Account {0} already exists. Please enter a different name for your bank account.', - [me.values.bank_account] - ); - frappe.msgprint(message); - } - } - }); - return !exist; // Return False if exist = true - } - return true; }, From 72e602ae548a4dd16ace8788f574c09374941b68 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 18:22:29 +0530 Subject: [PATCH 010/550] fix: Add validation for GST Settings --- .../regional/doctype/gst_settings/gst_settings.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.py b/erpnext/regional/doctype/gst_settings/gst_settings.py index bc956e9fa8..af3d92e59a 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.py +++ b/erpnext/regional/doctype/gst_settings/gst_settings.py @@ -19,6 +19,21 @@ class GSTSettings(Document): from tabAddress where country = "India" and ifnull(gstin, '')!='' ''') self.set_onload('data', data) + def validate(self): + # Validate duplicate accounts + self.validate_duplicate_accounts() + + def validate_duplicate_accounts(self): + account_list = [] + for account in self.get('gst_accounts'): + for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']: + if account.get(fieldname) in account_list: + frappe.throw(_("Account {0} appears multiple times").format( + frappe.bold(account.get(fieldname)))) + + if account.get(fieldname): + account_list.append(account.get(fieldname)) + @frappe.whitelist() def send_reminder(): frappe.has_permission('GST Settings', throw=True) From 1bac72b74d41a7284ca5028015a386b6cdaa61b5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 22:29:48 +0530 Subject: [PATCH 011/550] fix: Add GST accounts to GST Settings --- erpnext/regional/india/setup.py | 52 ++++++++++++++++++++++++ erpnext/setup/doctype/company/company.py | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 71b5661a4e..2c0a285546 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -15,6 +15,7 @@ def setup(company=None, patch=True): setup_company_independent_fixtures(patch=patch) if not patch: make_fixtures(company) + setup_gst_settings(company) # TODO: for all countries def setup_company_independent_fixtures(patch=False): @@ -664,6 +665,57 @@ def make_fixtures(company=None): # create records for Tax Withholding Category set_tax_withholding_category(company) +def setup_gst_settings(company): + # Will only add default GST accounts if present + input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] + output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] + gst_settings = frappe.get_single('GST Settings') + existing_account_list = [] + + for account in gst_settings.get('gst_accounts'): + for key in ['cgst_account', 'sgst_account', 'igst_account']: + existing_account_list.append(account.get(key)) + + gst_accounts = frappe._dict(frappe.get_all("Account", + {'company': company, 'name': ('like', "%GST%")}, ['account_name', 'name'], as_list=1)) + + all_input_account_exists = 0 + all_output_account_exists = 0 + + for account in input_account_names: + if not gst_accounts.get(account): + all_input_account_exists = 1 + + # Check if already added in GST Settings + if gst_accounts.get(account) in existing_account_list: + all_input_account_exists = 1 + + for account in output_account_names: + if not gst_accounts.get(account): + all_output_account_exists = 1 + + # Check if already added in GST Settings + if gst_accounts.get(account) in existing_account_list: + all_output_account_exists = 1 + + if not all_input_account_exists: + gst_settings.append('gst_accounts', { + 'company': company, + 'cgst_account': gst_accounts.get(input_account_names[0]), + 'sgst_account': gst_accounts.get(input_account_names[1]), + 'igst_account': gst_accounts.get(input_account_names[2]) + }) + + if not all_output_account_exists: + gst_settings.append('gst_accounts', { + 'company': company, + 'cgst_account': gst_accounts.get(output_account_names[0]), + 'sgst_account': gst_accounts.get(output_account_names[1]), + 'igst_account': gst_accounts.get(output_account_names[2]) + }) + + gst_settings.save() + def set_salary_components(docs): docs.extend([ {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 779e976181..9a74a2e68e 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -443,7 +443,7 @@ def install_country_fixtures(company, country): frappe.get_attr(module_name)(company, False) except Exception as e: frappe.log_error() - frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country))) + frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country))) def update_company_current_month_sales(company): From ed79b224ccaf482d6c3562cca99dc484dad73fd2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 20:52:02 +0530 Subject: [PATCH 012/550] fix(Asset): Group buttons under 'Action' --- erpnext/assets/doctype/asset/asset.js | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 6f1bb28f37..657c169fd1 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -78,36 +78,36 @@ frappe.ui.form.on('Asset', { frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.events.make_schedules_editable(frm); + if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { + frm.add_custom_button("General Ledger", function() { + frappe.route_options = { + "voucher_no": frm.doc.name, + "from_date": frm.doc.available_for_use_date, + "to_date": frm.doc.available_for_use_date, + "company": frm.doc.company + }; + frappe.set_route("query-report", "General Ledger"); + }); + } + if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { frm.add_custom_button("Transfer Asset", function() { erpnext.asset.transfer_asset(frm); - }); + }, __("Actions")); frm.add_custom_button("Scrap Asset", function() { erpnext.asset.scrap_asset(frm); - }); + }, __("Actions")); frm.add_custom_button("Sell Asset", function() { frm.trigger("make_sales_invoice"); - }); + }, __("Actions")); } else if (frm.doc.status=='Scrapped') { frm.add_custom_button("Restore Asset", function() { erpnext.asset.restore_asset(frm); - }); - } - - if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("General Ledger", function() { - frappe.route_options = { - "voucher_no": frm.doc.name, - "from_date": frm.doc.available_for_use_date, - "to_date": frm.doc.available_for_use_date, - "company": frm.doc.company - }; - frappe.set_route("query-report", "General Ledger"); - }); + }, __("Actions")); } if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { From 322975a03c98bfa72d3372c9cadca0be892855fb Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 21:09:13 +0530 Subject: [PATCH 013/550] feat(Asset): Add 'Create > Asset Repair' button --- erpnext/assets/doctype/asset/asset.js | 24 ++++++++++++++++++++++++ erpnext/assets/doctype/asset/asset.py | 12 +++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 657c169fd1..1f3978c0b0 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -115,6 +115,15 @@ frappe.ui.form.on('Asset', { frm.trigger("create_asset_maintenance"); }, __('Create')); } + if (frm.doc.docstatus == 1) { + frm.add_custom_button(__("Asset Repair"), function() { + // frappe.model.open_mapped_doc({ + // method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.make_expense_claim', + // frm: cur_frm, + // }); + frm.trigger("create_asset_repair"); + }, __("Create")); + } if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Asset Value Adjustment"), function() { frm.trigger("create_asset_adjustment"); @@ -304,6 +313,21 @@ frappe.ui.form.on('Asset', { }) }, + create_asset_repair: function(frm) { + frappe.call({ + args: { + "asset": frm.doc.name, + "item_code": frm.doc.item_code, + "item_name": frm.doc.item_name + }, + method: "erpnext.assets.doctype.asset.asset.create_asset_repair", + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }) + }, + create_asset_adjustment: function(frm) { frappe.call({ args: { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9aff1440d6..962f78fa66 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -637,9 +637,19 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan }) return asset_maintenance +@frappe.whitelist() +def create_asset_repair(asset, item_code, item_name): + asset_repair = frappe.new_doc("Asset Repair") + asset_repair.update({ + "asset_name": asset, + "item_code": item_code, + "item_name": item_name + }) + return asset_repair + @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.new_doc("Asset Value Adjustment") + asset_maintenance = frappe.get_doc("Asset Value Adjustment") asset_maintenance.update({ "asset": asset, "company": company, From 3e0e3f0e1b062e38312950bc26b2ca29c86ee47b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 23:08:12 +0530 Subject: [PATCH 014/550] fix(Asset Repair): Replace 'Item Name' and 'Item Code' with 'Asset Name' --- erpnext/assets/doctype/asset/asset.js | 3 +- erpnext/assets/doctype/asset/asset.py | 7 ++-- .../doctype/asset_repair/asset_repair.json | 42 ++++++++----------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 1f3978c0b0..92a6648c63 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -317,8 +317,7 @@ frappe.ui.form.on('Asset', { frappe.call({ args: { "asset": frm.doc.name, - "item_code": frm.doc.item_code, - "item_name": frm.doc.item_name + "asset_name": frm.doc.asset_name }, method: "erpnext.assets.doctype.asset.asset.create_asset_repair", callback: function(r) { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 962f78fa66..b9f413854f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -638,12 +638,11 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan return asset_maintenance @frappe.whitelist() -def create_asset_repair(asset, item_code, item_name): +def create_asset_repair(asset, asset_name): asset_repair = frappe.new_doc("Asset Repair") asset_repair.update({ - "asset_name": asset, - "item_code": item_code, - "item_name": item_name + "asset": asset, + "asset_name": asset_name }) return asset_repair diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index d338fc0fb7..853534eb31 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -8,10 +8,9 @@ "engine": "InnoDB", "field_order": [ "naming_series", - "asset_name", "column_break_2", - "item_code", - "item_name", + "asset", + "asset_name", "section_break_5", "failure_date", "assign_to", @@ -30,15 +29,6 @@ "amended_from" ], "fields": [ - { - "columns": 1, - "fieldname": "asset_name", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Asset", - "options": "Asset", - "reqd": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -50,18 +40,6 @@ "fieldname": "column_break_2", "fieldtype": "Column Break" }, - { - "fetch_from": "asset_name.item_code", - "fieldname": "item_code", - "fieldtype": "Read Only", - "label": "Item Code" - }, - { - "fetch_from": "asset_name.item_name", - "fieldname": "item_name", - "fieldtype": "Read Only", - "label": "Item Name" - }, { "fieldname": "section_break_5", "fieldtype": "Section Break", @@ -159,12 +137,26 @@ "options": "Asset Repair", "print_hide": 1, "read_only": 1 + }, + { + "columns": 1, + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fieldname": "asset_name", + "fieldtype": "Read Only", + "label": "Asset Name" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-01-22 15:08:12.495850", + "modified": "2021-05-10 22:48:42.165513", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From bbbd61dab8830a7be0689b7d7b26f4873ee452dc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 10 May 2021 23:24:34 +0530 Subject: [PATCH 015/550] fix(Asset): Group all buttons under 'Manage' --- erpnext/assets/doctype/asset/asset.js | 60 +++++++++++++-------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 92a6648c63..2a57183a80 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -78,65 +78,61 @@ frappe.ui.form.on('Asset', { frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.events.make_schedules_editable(frm); - if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { - frm.add_custom_button("General Ledger", function() { - frappe.route_options = { - "voucher_no": frm.doc.name, - "from_date": frm.doc.available_for_use_date, - "to_date": frm.doc.available_for_use_date, - "company": frm.doc.company - }; - frappe.set_route("query-report", "General Ledger"); - }); - } - if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { frm.add_custom_button("Transfer Asset", function() { erpnext.asset.transfer_asset(frm); - }, __("Actions")); + }, __("Manage")); frm.add_custom_button("Scrap Asset", function() { erpnext.asset.scrap_asset(frm); - }, __("Actions")); + }, __("Manage")); frm.add_custom_button("Sell Asset", function() { frm.trigger("make_sales_invoice"); - }, __("Actions")); + }, __("Manage")); } else if (frm.doc.status=='Scrapped') { frm.add_custom_button("Restore Asset", function() { erpnext.asset.restore_asset(frm); - }, __("Actions")); + }, __("Manage")); } if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { - frm.add_custom_button(__("Asset Maintenance"), function() { + frm.add_custom_button(__("Maintain Asset"), function() { frm.trigger("create_asset_maintenance"); - }, __('Create')); - } - if (frm.doc.docstatus == 1) { - frm.add_custom_button(__("Asset Repair"), function() { - // frappe.model.open_mapped_doc({ - // method: 'erpnext.stock.doctype.delivery_trip.delivery_trip.make_expense_claim', - // frm: cur_frm, - // }); - frm.trigger("create_asset_repair"); - }, __("Create")); + }, __("Manage")); } + + frm.add_custom_button(__("Repair Asset"), function() { + frm.trigger("create_asset_repair"); + }, __("Manage")); + if (frm.doc.status != 'Fully Depreciated') { - frm.add_custom_button(__("Asset Value Adjustment"), function() { + frm.add_custom_button(__("Adjust Asset Value"), function() { frm.trigger("create_asset_adjustment"); - }, __('Create')); + }, __("Manage")); } if (!frm.doc.calculate_depreciation) { - frm.add_custom_button(__("Depreciation Entry"), function() { + frm.add_custom_button(__("Create Depreciation Entry"), function() { frm.trigger("make_journal_entry"); - }, __('Create')); + }, __("Manage")); } - frm.page.set_inner_btn_group_as_primary(__('Create')); + if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { + frm.add_custom_button("View General Ledger", function() { + frappe.route_options = { + "voucher_no": frm.doc.name, + "from_date": frm.doc.available_for_use_date, + "to_date": frm.doc.available_for_use_date, + "company": frm.doc.company + }; + frappe.set_route("query-report", "General Ledger"); + }, __("Manage")); + } + + frm.page.set_inner_btn_group_as_primary(__("Manage")); frm.trigger("setup_chart"); } From 1c26b27a8929667961d1a587db14bc74f480eb3f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 00:08:39 +0530 Subject: [PATCH 016/550] fix(Asset Repair): Display 'Completion Date' and 'Repair Status' only after save --- erpnext/assets/doctype/asset_repair/asset_repair.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 4ba2b4474a..f5eeeda5fb 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,6 +2,10 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { + refresh: function(frm) { + frm.toggle_display(['completion_date', 'repair_status'], !(frm.doc.__islocal)); + }, + repair_status: (frm) => { if (frm.doc.completion_date && frm.doc.repair_status == "Completed") { frappe.call ({ From 30c4a56491f9aa86f491ced75d0d2eb70f302d3b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 18:43:36 +0530 Subject: [PATCH 017/550] feat(Asset Repair): Change Asset's status according to repair_status --- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 049b931b5e..884dc19588 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -13,6 +13,10 @@ class AssetRepair(Document): if self.repair_status == "Completed" and not self.completion_date: frappe.throw(_("Please select Completion Date for Completed Repair")) + if self.repair_status == 'Pending': + frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') + else: + frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') @frappe.whitelist() def get_downtime(failure_date, completion_date): From deadcd9e97c201bff3d36b237c47c2054a7c2297 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 21:43:56 +0530 Subject: [PATCH 018/550] feat(Asset Repair): Add payable_account field --- .../doctype/asset_repair/asset_repair.json | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 853534eb31..4ed9916392 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -7,9 +7,9 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "asset", "naming_series", "column_break_2", - "asset", "asset_name", "section_break_5", "failure_date", @@ -18,7 +18,10 @@ "column_break_6", "completion_date", "repair_status", + "section_break_7", "repair_cost", + "column_break_8", + "payable_account", "section_break_9", "description", "column_break_9", @@ -151,12 +154,27 @@ "fieldname": "asset_name", "fieldtype": "Read Only", "label": "Asset Name" + }, + { + "fieldname": "payable_account", + "fieldtype": "Link", + "label": "Payable Account", + "options": "Account" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-10 22:48:42.165513", + "modified": "2021-05-11 05:11:58.330860", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 88ac9b2ec9862fcb5ed901f389baef9a8a97511c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 21:46:49 +0530 Subject: [PATCH 019/550] fix(Company): Rename 'Fixed Asset Depreciation Settings' to 'Fixed Asset Deafults' --- erpnext/setup/doctype/company/company.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 83cbf475ab..acd05c43e0 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -74,7 +74,7 @@ "stock_received_but_not_billed", "service_received_but_not_billed", "expenses_included_in_valuation", - "fixed_asset_depreciation_settings", + "fixed_asset_defaults", "accumulated_depreciation_account", "depreciation_expense_account", "series_for_depreciation_entry", @@ -520,12 +520,6 @@ "no_copy": 1, "options": "Account" }, - { - "collapsible": 1, - "fieldname": "fixed_asset_depreciation_settings", - "fieldtype": "Section Break", - "label": "Fixed Asset Depreciation Settings" - }, { "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", @@ -740,6 +734,12 @@ "fieldtype": "Link", "label": "Default Payment Discount Account", "options": "Account" + }, + { + "collapsible": 1, + "fieldname": "fixed_asset_defaults", + "fieldtype": "Section Break", + "label": "Fixed Asset Defaults" } ], "icon": "fa fa-building", @@ -747,7 +747,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-02-16 15:53:37.167589", + "modified": "2021-05-11 21:45:22.803065", "modified_by": "Administrator", "module": "Setup", "name": "Company", From 11594f870ea58ba0984956acd6d4abd9f7183948 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 21:47:58 +0530 Subject: [PATCH 020/550] fix(Company): Add 'Repair and Maintenance Account' field --- erpnext/setup/doctype/company/company.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index acd05c43e0..0bc85ea092 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -83,6 +83,7 @@ "disposal_account", "depreciation_cost_center", "capital_work_in_progress_account", + "repair_and_maintenance_account", "asset_received_but_not_billed", "budget_detail", "exception_budget_approver_role", @@ -740,6 +741,11 @@ "fieldname": "fixed_asset_defaults", "fieldtype": "Section Break", "label": "Fixed Asset Defaults" + }, + { + "fieldname": "repair_and_maintenance_account", + "fieldtype": "Data", + "label": "Repair and Maintenance Account" } ], "icon": "fa fa-building", @@ -747,7 +753,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-05-11 21:45:22.803065", + "modified": "2021-05-11 21:47:04.667950", "modified_by": "Administrator", "module": "Setup", "name": "Company", From 1b2a5d1489d4e034dc04d0970b253cc1b6b89b3e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 23:33:07 +0530 Subject: [PATCH 021/550] feat(Asset Repair): Add 'Capitalize Repair Cost' checkbox --- .../doctype/asset_repair/asset_repair.json | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 4ed9916392..8762451dd9 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -18,8 +18,9 @@ "column_break_6", "completion_date", "repair_status", - "section_break_7", + "accounting_details", "repair_cost", + "capitalize_repair_cost", "column_break_8", "payable_account", "section_break_9", @@ -161,20 +162,26 @@ "label": "Payable Account", "options": "Account" }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, { "fieldname": "column_break_8", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "capitalize_repair_cost", + "fieldtype": "Check", + "label": "Capitalize Repair Cost" + }, + { + "fieldname": "accounting_details", + "fieldtype": "Section Break", + "label": "Accounting Details" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-11 05:11:58.330860", + "modified": "2021-05-11 23:25:48.956382", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 194a08e4ec1fbff44da24d653182ce27217368b5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 23:34:21 +0530 Subject: [PATCH 022/550] fix(Asset Repair): Hide 'Accounting Details' section till doc gets saved --- .../assets/doctype/asset_repair/asset_repair.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index f5eeeda5fb..23bc7b1891 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,8 +2,23 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { + // setup: function(frm) { + // frm.add_fetch("company", "repair_and_maintenance_account", "payable_account"); + + // frm.set_query("payable_account", function() { + // return { + // filters: { + // "report_type": "Balance Sheet", + // "account_type": "Payable", + // "company": frm.doc.company, + // "is_group": 0 + // } + // }; + // }); + // }, + refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details'], !(frm.doc.__islocal)); }, repair_status: (frm) => { From 6d1cf76b77c1264e67085e2d5d08829e0b8afb67 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 11 May 2021 23:56:34 +0530 Subject: [PATCH 023/550] feat(Asset): Add 'Asset Value' field --- erpnext/assets/doctype/asset/asset.json | 9 ++++++++- erpnext/assets/doctype/asset/asset.py | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 421b9a6c37..4960c7e709 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -23,6 +23,7 @@ "asset_name", "asset_category", "location", + "asset_value", "custodian", "department", "disposal_date", @@ -480,6 +481,12 @@ "fieldname": "section_break_36", "fieldtype": "Section Break", "label": "Finance Books" + }, + { + "fieldname": "asset_value", + "fieldtype": "Currency", + "label": "Asset Value", + "read_only": 1 } ], "idx": 72, @@ -502,7 +509,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-01-22 12:38:59.091510", + "modified": "2021-05-11 23:47:15.831720", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index b9f413854f..350220b897 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -96,6 +96,9 @@ class Asset(AccountsController): finance_books = get_item_details(self.item_code, self.asset_category) self.set('finance_books', finance_books) + if not(self.asset_value): + self.asset_value = self.gross_purchase_amount + def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") From 258c2385836a844282aa657b2c21de4550b8752f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 00:15:18 +0530 Subject: [PATCH 024/550] feat(Asset Repair): Add repair_cost to asset_value --- .../doctype/asset_repair/asset_repair.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 884dc19588..b233358e80 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -7,6 +7,9 @@ import frappe from frappe import _ from frappe.utils import time_diff_in_hours from frappe.model.document import Document +from frappe.utils import flt +from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.controllers.accounts_controller import AccountsController class AssetRepair(Document): def validate(self): @@ -18,6 +21,73 @@ class AssetRepair(Document): else: frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') + def on_submit(self): + self.increase_asset_value() + + def increase_asset_value(self): + if self.capitalize_repair_cost: + asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + + # self.make_gl_entries() + + # def on_cancel(self): + # if self.payable_account: + # self.make_gl_entries(cancel=True) + + # def make_gl_entries(self, cancel=False): + # if flt(self.repair_cost) > 0: + # gl_entries = self.get_gl_entries() + # make_gl_entries(gl_entries, cancel) + + # def get_gl_entries(self): + # gl_entry = [] + # company = frappe.db.get_value('Asset', self.asset, 'company') + # repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') + + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": self.payable_account, + # "credit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": repair_and_maintenance_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name + # }) + # gl_entry.insert() + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": repair_and_maintenance_account, + # "debit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": self.payable_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name + # }) + # gl_entry.insert() + + # gl_entry.append( + # self.get_gl_dict({ + # "account": self.payable_account, + # "credit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": repair_and_maintenance_account, + # "against_voucher_type": self.doctype, + # "against_voucher": self.name + # }) + # ) + + # gl_entry.append( + # self.get_gl_dict({ + # "account": repair_and_maintenance_account, + # "debit": self.repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": self.payable_account, + # "against_voucher_type": self.doctype, + # "against_voucher": self.name + # }) + # ) + @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) From c62890bcdbabd610497c76f8ddacd60f12222ef1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 03:27:55 +0530 Subject: [PATCH 025/550] feat: Create 'Stock Item' child table --- erpnext/assets/doctype/stock_item/__init__.py | 0 .../assets/doctype/stock_item/stock_item.json | 55 +++++++++++++++++++ .../assets/doctype/stock_item/stock_item.py | 8 +++ 3 files changed, 63 insertions(+) create mode 100644 erpnext/assets/doctype/stock_item/__init__.py create mode 100644 erpnext/assets/doctype/stock_item/stock_item.json create mode 100644 erpnext/assets/doctype/stock_item/stock_item.py diff --git a/erpnext/assets/doctype/stock_item/__init__.py b/erpnext/assets/doctype/stock_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/assets/doctype/stock_item/stock_item.json b/erpnext/assets/doctype/stock_item/stock_item.json new file mode 100644 index 0000000000..b1f05db395 --- /dev/null +++ b/erpnext/assets/doctype/stock_item/stock_item.json @@ -0,0 +1,55 @@ +{ + "actions": [], + "creation": "2021-05-12 02:41:54.161024", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item", + "valuation_rate", + "consumed_quantity", + "total_value" + ], + "fields": [ + { + "fieldname": "item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item", + "options": "Item" + }, + { + "fetch_from": "item.valuation_rate", + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Valuation Rate", + "read_only": 1 + }, + { + "fieldname": "consumed_quantity", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumed Quantity" + }, + { + "fieldname": "total_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Value", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-05-12 03:19:55.006300", + "modified_by": "Administrator", + "module": "Assets", + "name": "Stock Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/stock_item/stock_item.py b/erpnext/assets/doctype/stock_item/stock_item.py new file mode 100644 index 0000000000..0e3cc3f8ba --- /dev/null +++ b/erpnext/assets/doctype/stock_item/stock_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class StockItem(Document): + pass From d2d31fd16424268140dc75c7611774bbf8cb5dab Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 03:28:45 +0530 Subject: [PATCH 026/550] feat(Asset Repair): Add 'Stock Items' table --- .../doctype/asset_repair/asset_repair.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 8762451dd9..e82c5f2926 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -23,11 +23,13 @@ "capitalize_repair_cost", "column_break_8", "payable_account", + "section_break_17", + "stock_items", "section_break_9", "description", "column_break_9", "actions_performed", - "section_break_17", + "section_break_23", "downtime", "column_break_19", "amended_from" @@ -176,12 +178,22 @@ "fieldname": "accounting_details", "fieldtype": "Section Break", "label": "Accounting Details" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "options": "Stock Item" + }, + { + "fieldname": "section_break_23", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-11 23:25:48.956382", + "modified": "2021-05-12 03:23:30.618741", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 0739d4bb1b3c79734f1eec03194b504fb7886888 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 05:15:26 +0530 Subject: [PATCH 027/550] feat(Asset Repair): Add value of consumed stock items to asset value --- .../assets/doctype/asset_repair/asset_repair.js | 11 ++++++++++- .../doctype/asset_repair/asset_repair.json | 12 ++++++------ .../assets/doctype/asset_repair/asset_repair.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 23bc7b1891..4641f3aaa2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -17,8 +17,17 @@ frappe.ui.form.on('Asset Repair', { // }); // }, + // stock_items_add: function(frm){ + // var table = frm.doc.stock_items; + // for(var i in table) { + // if (table[i].valuation_rate == 0) { + // frm.set_value(table[i].total_value, (table[i].valuation_rate * table[i].consumed_quantity)) + // } + // } + // }, + refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section'], !(frm.doc.__islocal)); }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index e82c5f2926..a8a9f2d195 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -23,7 +23,7 @@ "capitalize_repair_cost", "column_break_8", "payable_account", - "section_break_17", + "stock_items_section", "stock_items", "section_break_9", "description", @@ -113,10 +113,6 @@ "fieldtype": "Long Text", "label": "Actions performed" }, - { - "fieldname": "section_break_17", - "fieldtype": "Section Break" - }, { "allow_on_submit": 1, "fieldname": "downtime", @@ -188,12 +184,16 @@ { "fieldname": "section_break_23", "fieldtype": "Section Break" + }, + { + "fieldname": "stock_items_section", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 03:23:30.618741", + "modified": "2021-05-12 04:45:38.714776", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index b233358e80..0980ddfa7f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -16,17 +16,33 @@ class AssetRepair(Document): if self.repair_status == "Completed" and not self.completion_date: frappe.throw(_("Please select Completion Date for Completed Repair")) + self.update_status() + self.set_total_value() + + def set_total_value(self): + for item in self.stock_items: + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + + def update_status(self): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') def on_submit(self): + if self.repair_status == "Pending": + frappe.throw(_("Please update Repair Status.")) + self.increase_asset_value() def increase_asset_value(self): if self.capitalize_repair_cost: asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost + for item in self.stock_items: + asset_value += item.total_value + + print("*" * 20) + print(asset_value) frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) # self.make_gl_entries() From e5ab5d8963efdc6848adf935eea122341aa1295c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 20:39:53 +0530 Subject: [PATCH 028/550] feat(Asset Repair): Create GL Entries --- .../doctype/asset_repair/asset_repair.py | 89 +++++++------------ erpnext/setup/doctype/company/company.json | 7 +- 2 files changed, 38 insertions(+), 58 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 0980ddfa7f..d34970d16d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -34,6 +34,7 @@ class AssetRepair(Document): frappe.throw(_("Please update Repair Status.")) self.increase_asset_value() + self.make_gl_entries() def increase_asset_value(self): if self.capitalize_repair_cost: @@ -45,64 +46,42 @@ class AssetRepair(Document): print(asset_value) frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) - # self.make_gl_entries() + def on_cancel(self): + if self.payable_account: + self.make_gl_entries(cancel=True) - # def on_cancel(self): - # if self.payable_account: - # self.make_gl_entries(cancel=True) + def make_gl_entries(self, cancel=False): + if flt(self.repair_cost) > 0: + gl_entries = self.get_gl_entries() + make_gl_entries(gl_entries, cancel) - # def make_gl_entries(self, cancel=False): - # if flt(self.repair_cost) > 0: - # gl_entries = self.get_gl_entries() - # make_gl_entries(gl_entries, cancel) + def get_gl_entries(self): + gl_entry = [] + company = frappe.db.get_value('Asset', self.asset, 'company') + repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') - # def get_gl_entries(self): - # gl_entry = [] - # company = frappe.db.get_value('Asset', self.asset, 'company') - # repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') - - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": self.payable_account, - # "credit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": repair_and_maintenance_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name - # }) - # gl_entry.insert() - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": repair_and_maintenance_account, - # "debit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": self.payable_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name - # }) - # gl_entry.insert() - - # gl_entry.append( - # self.get_gl_dict({ - # "account": self.payable_account, - # "credit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": repair_and_maintenance_account, - # "against_voucher_type": self.doctype, - # "against_voucher": self.name - # }) - # ) - - # gl_entry.append( - # self.get_gl_dict({ - # "account": repair_and_maintenance_account, - # "debit": self.repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": self.payable_account, - # "against_voucher_type": self.doctype, - # "against_voucher": self.name - # }) - # ) + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": self.payable_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": "Main - F" + }) + gl_entry.insert() + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": repair_and_maintenance_account, + "debit": self.repair_cost, + "debit_in_account_currency": self.repair_cost, + "against": self.payable_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": "Main - F" + }) + gl_entry.insert() @frappe.whitelist() def get_downtime(failure_date, completion_date): diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 0bc85ea092..51757f584e 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -744,8 +744,9 @@ }, { "fieldname": "repair_and_maintenance_account", - "fieldtype": "Data", - "label": "Repair and Maintenance Account" + "fieldtype": "Link", + "label": "Repair and Maintenance Account", + "options": "Account" } ], "icon": "fa fa-building", @@ -753,7 +754,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-05-11 21:47:04.667950", + "modified": "2021-05-12 16:51:08.187233", "modified_by": "Administrator", "module": "Setup", "name": "Company", From f5a2ea9cb370727e9e1f5e3da2cf057155872f4d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:06:20 +0530 Subject: [PATCH 029/550] feat(Asset Repair): Add 'Accounting Dimensions' section --- .../doctype/asset_repair/asset_repair.js | 2 +- .../doctype/asset_repair/asset_repair.json | 27 ++++++++++++++++++- .../doctype/asset_repair/asset_repair.py | 4 +-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 4641f3aaa2..d61cd83e3e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -27,7 +27,7 @@ frappe.ui.form.on('Asset Repair', { // }, refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section', 'accounting_dimensions_section'], !(frm.doc.__islocal)); }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index a8a9f2d195..18bb2da658 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -18,6 +18,10 @@ "column_break_6", "completion_date", "repair_status", + "accounting_dimensions_section", + "cost_center", + "column_break_14", + "project", "accounting_details", "repair_cost", "capitalize_repair_cost", @@ -188,12 +192,33 @@ { "fieldname": "stock_items_section", "fieldtype": "Section Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 04:45:38.714776", + "modified": "2021-05-12 21:03:59.032864", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d34970d16d..e4cbabee36 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -41,9 +41,7 @@ class AssetRepair(Document): asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost for item in self.stock_items: asset_value += item.total_value - - print("*" * 20) - print(asset_value) + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) def on_cancel(self): From d554267b0567ef7e70a47708793fdab7aaaa30a5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:08:55 +0530 Subject: [PATCH 030/550] feat(Asset Repair): Add 'Cost Center' to GL Entries --- erpnext/assets/doctype/asset_repair/asset_repair.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index e4cbabee36..343e6c4c9c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -41,7 +41,7 @@ class AssetRepair(Document): asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost for item in self.stock_items: asset_value += item.total_value - + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) def on_cancel(self): @@ -66,7 +66,7 @@ class AssetRepair(Document): "against": repair_and_maintenance_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": "Main - F" + "cost_center": self.cost_center }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -77,7 +77,7 @@ class AssetRepair(Document): "against": self.payable_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": "Main - F" + "cost_center": self.cost_center }) gl_entry.insert() From 234b473d9316ed3c536821a5079cb9d293118bf4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:10:16 +0530 Subject: [PATCH 031/550] feat(Asset Repair): Remove 'Assign To' --- .../doctype/asset_repair/asset_repair.json | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 18bb2da658..cda0edf1c5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -13,8 +13,6 @@ "asset_name", "section_break_5", "failure_date", - "assign_to", - "assign_to_name", "column_break_6", "completion_date", "repair_status", @@ -62,20 +60,6 @@ "label": "Failure Date", "reqd": 1 }, - { - "allow_on_submit": 1, - "fieldname": "assign_to", - "fieldtype": "Link", - "label": "Assign To", - "options": "User" - }, - { - "allow_on_submit": 1, - "fetch_from": "assign_to.full_name", - "fieldname": "assign_to_name", - "fieldtype": "Read Only", - "label": "Assign To Name" - }, { "fieldname": "column_break_6", "fieldtype": "Column Break" @@ -218,7 +202,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 21:03:59.032864", + "modified": "2021-05-12 21:09:27.994356", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 7ad74cf8004092fe1be44f455bdcdafcbaecfa7f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 12 May 2021 21:22:36 +0530 Subject: [PATCH 032/550] feat(Asset Repair): Add stock_consumption checkbox --- .../doctype/asset_repair/asset_repair.js | 3 ++- .../doctype/asset_repair/asset_repair.json | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index d61cd83e3e..605edccc11 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -27,7 +27,8 @@ frappe.ui.form.on('Asset Repair', { // }, refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'stock_items_section', 'accounting_dimensions_section'], !(frm.doc.__islocal)); + frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); + frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index cda0edf1c5..9ab9271fa9 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -23,9 +23,10 @@ "accounting_details", "repair_cost", "capitalize_repair_cost", + "stock_consumption", "column_break_8", "payable_account", - "stock_items_section", + "stock_consumption_details_section", "stock_items", "section_break_9", "description", @@ -173,10 +174,6 @@ "fieldname": "section_break_23", "fieldtype": "Section Break" }, - { - "fieldname": "stock_items_section", - "fieldtype": "Section Break" - }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", @@ -197,12 +194,23 @@ { "fieldname": "column_break_14", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "stock_consumption", + "fieldtype": "Check", + "label": "Stock Consumed During Repair" + }, + { + "fieldname": "stock_consumption_details_section", + "fieldtype": "Section Break", + "label": "Stock Consumption Details" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 21:09:27.994356", + "modified": "2021-05-12 21:20:18.276755", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From c2b0102852acc1d826fdf9bc01ca7c9a75dbca6b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 00:58:07 +0530 Subject: [PATCH 033/550] feat(Asset Repair): Add 'View General Ledger' button --- erpnext/assets/doctype/asset_repair/asset_repair.js | 9 +++++++++ erpnext/assets/doctype/asset_repair/asset_repair.py | 9 +++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 605edccc11..a5fda53429 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -29,6 +29,15 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) + + if (frm.doc.docstatus) { + frm.add_custom_button("View General Ledger", function() { + frappe.route_options = { + "voucher_no": frm.doc.name + }; + frappe.set_route("query-report", "General Ledger"); + }); + } }, repair_status: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 343e6c4c9c..9c3d880eac 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,11 +5,10 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours +from frappe.utils import time_diff_in_hours, getdate from frappe.model.document import Document from frappe.utils import flt from erpnext.accounts.general_ledger import make_gl_entries -from erpnext.controllers.accounts_controller import AccountsController class AssetRepair(Document): def validate(self): @@ -66,7 +65,8 @@ class AssetRepair(Document): "against": repair_and_maintenance_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": self.cost_center + "cost_center": self.cost_center, + "posting_date": getdate() }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -77,7 +77,8 @@ class AssetRepair(Document): "against": self.payable_account, "voucher_type": self.doctype, "voucher_no": self.name, - "cost_center": self.cost_center + "cost_center": self.cost_center, + "posting_date": getdate() }) gl_entry.insert() From 385ca80ceb939fe76fecb5fcfc95742345729b04 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 02:53:57 +0530 Subject: [PATCH 034/550] feat(Asset Repair): Add total_repair_cost field --- .../doctype/asset_repair/asset_repair.js | 2 +- .../doctype/asset_repair/asset_repair.json | 8 ++- .../doctype/asset_repair/asset_repair.py | 60 +++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index a5fda53429..9d06caeb98 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -28,7 +28,7 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) + frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption) if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 9ab9271fa9..df9ab7d63b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -26,6 +26,7 @@ "stock_consumption", "column_break_8", "payable_account", + "total_repair_cost", "stock_consumption_details_section", "stock_items", "section_break_9", @@ -205,12 +206,17 @@ "fieldname": "stock_consumption_details_section", "fieldtype": "Section Break", "label": "Stock Consumption Details" + }, + { + "fieldname": "total_repair_cost", + "fieldtype": "Currency", + "label": "Total Repair Cost" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 21:20:18.276755", + "modified": "2021-05-13 02:40:57.953076", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9c3d880eac..2b69b5b110 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -16,18 +16,31 @@ class AssetRepair(Document): frappe.throw(_("Please select Completion Date for Completed Repair")) self.update_status() - self.set_total_value() - - def set_total_value(self): - for item in self.stock_items: - item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) - + self.set_total_value() # change later + self.check_stock_items() + self.calculate_total_repair_cost() + def update_status(self): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') + def set_total_value(self): + for item in self.stock_items: + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + + def check_stock_items(self): + if self.stock_consumption: + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + + def calculate_total_repair_cost(self): + self.total_repair_cost = self.repair_cost + if self.stock_consumption: + for item in self.stock_items: + self.total_repair_cost += item.total_value + def on_submit(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) @@ -81,6 +94,41 @@ class AssetRepair(Document): "posting_date": getdate() }) gl_entry.insert() + + # if self.capitalize_repair_cost: + # fixed_asset_account = self.get_fixed_asset_account() + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": self.payable_account, + # "credit": self.total_repair_cost, + # "credit_in_account_currency": self.repair_cost, + # "against": repair_and_maintenance_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name, + # "cost_center": self.cost_center, + # "posting_date": getdate() + # }) + # gl_entry.insert() + # gl_entry = frappe.get_doc({ + # "doctype": "GL Entry", + # "account": fixed_asset_account, + # "debit": self.total_repair_cost, + # "debit_in_account_currency": self.repair_cost, + # "against": self.payable_account, + # "voucher_type": self.doctype, + # "voucher_no": self.name, + # "cost_center": self.cost_center, + # "posting_date": getdate() + # }) + # gl_entry.insert() + + # def get_fixed_asset_account(self): + # asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) + # company = frappe.db.get_value('Asset', self.asset, 'company') + # for account in asset_category.accounts: + # if account.company_name == company: + # return account.fixed_asset_account + @frappe.whitelist() def get_downtime(failure_date, completion_date): From 8d05eca45e8e3dc6ed17d0471d278ef4f9226a39 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 03:26:03 +0530 Subject: [PATCH 035/550] feat(Asset Repair): Create GL Entries to capitalise repair cost --- .../doctype/asset_repair/asset_repair.py | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 2b69b5b110..12dda1d4a1 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -73,8 +73,8 @@ class AssetRepair(Document): gl_entry = frappe.get_doc({ "doctype": "GL Entry", "account": self.payable_account, - "credit": self.repair_cost, - "credit_in_account_currency": self.repair_cost, + "credit": self.total_repair_cost, + "credit_in_account_currency": self.total_repair_cost, "against": repair_and_maintenance_account, "voucher_type": self.doctype, "voucher_no": self.name, @@ -85,8 +85,8 @@ class AssetRepair(Document): gl_entry = frappe.get_doc({ "doctype": "GL Entry", "account": repair_and_maintenance_account, - "debit": self.repair_cost, - "debit_in_account_currency": self.repair_cost, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, "against": self.payable_account, "voucher_type": self.doctype, "voucher_no": self.name, @@ -95,39 +95,39 @@ class AssetRepair(Document): }) gl_entry.insert() - # if self.capitalize_repair_cost: - # fixed_asset_account = self.get_fixed_asset_account() - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": self.payable_account, - # "credit": self.total_repair_cost, - # "credit_in_account_currency": self.repair_cost, - # "against": repair_and_maintenance_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name, - # "cost_center": self.cost_center, - # "posting_date": getdate() - # }) - # gl_entry.insert() - # gl_entry = frappe.get_doc({ - # "doctype": "GL Entry", - # "account": fixed_asset_account, - # "debit": self.total_repair_cost, - # "debit_in_account_currency": self.repair_cost, - # "against": self.payable_account, - # "voucher_type": self.doctype, - # "voucher_no": self.name, - # "cost_center": self.cost_center, - # "posting_date": getdate() - # }) - # gl_entry.insert() + if self.capitalize_repair_cost: + fixed_asset_account = self.get_fixed_asset_account() + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": self.payable_account, + "credit": self.total_repair_cost, + "credit_in_account_currency": self.total_repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": "Asset", + "voucher_no": self.asset, + "cost_center": self.cost_center, + "posting_date": getdate() + }) + gl_entry.insert() + gl_entry = frappe.get_doc({ + "doctype": "GL Entry", + "account": fixed_asset_account, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, + "against": self.payable_account, + "voucher_type": "Asset", + "voucher_no": self.asset, + "cost_center": self.cost_center, + "posting_date": getdate() + }) + gl_entry.insert() - # def get_fixed_asset_account(self): - # asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) - # company = frappe.db.get_value('Asset', self.asset, 'company') - # for account in asset_category.accounts: - # if account.company_name == company: - # return account.fixed_asset_account + def get_fixed_asset_account(self): + asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) + company = frappe.db.get_value('Asset', self.asset, 'company') + for account in asset_category.accounts: + if account.company_name == company: + return account.fixed_asset_account @frappe.whitelist() From f2fe55ceeb608c34e4a8a0ab73d2511e36603dee Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 03:42:32 +0530 Subject: [PATCH 036/550] feat(Asset Repair): Check for Cost Center and Payable Account --- .../assets/doctype/asset_repair/asset_repair.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 12dda1d4a1..dcad1c6181 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -42,12 +42,25 @@ class AssetRepair(Document): self.total_repair_cost += item.total_value def on_submit(self): - if self.repair_status == "Pending": - frappe.throw(_("Please update Repair Status.")) + self.check_repair_status() + self.check_for_payable_account() + self.check_for_cost_center() self.increase_asset_value() self.make_gl_entries() + def check_repair_status(self): + if self.repair_status == "Pending": + frappe.throw(_("Please update Repair Status.")) + + def check_for_payable_account(self): + if not self.payable_account: + frappe.throw(_("Please enter Payable Account.")) + + def check_for_cost_center(self): + if not self.cost_center: + frappe.throw(_("Please enter Cost Center.")) + def increase_asset_value(self): if self.capitalize_repair_cost: asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost From c8cb96a38fb811d9a7f1a8355d5071726087be49 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 04:04:52 +0530 Subject: [PATCH 037/550] feat(Asset Repair): Add Warehouse field --- .../assets/doctype/asset_repair/asset_repair.json | 9 ++++++++- erpnext/assets/doctype/asset_repair/asset_repair.py | 12 ++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index df9ab7d63b..ed7708c8df 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -28,6 +28,7 @@ "payable_account", "total_repair_cost", "stock_consumption_details_section", + "warehouse", "stock_items", "section_break_9", "description", @@ -211,12 +212,18 @@ "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 02:40:57.953076", + "modified": "2021-05-13 03:50:39.146322", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index dcad1c6181..ed022fd808 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -17,7 +17,6 @@ class AssetRepair(Document): self.update_status() self.set_total_value() # change later - self.check_stock_items() self.calculate_total_repair_cost() def update_status(self): @@ -30,11 +29,6 @@ class AssetRepair(Document): for item in self.stock_items: item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) - def check_stock_items(self): - if self.stock_consumption: - if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) - def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost if self.stock_consumption: @@ -43,6 +37,7 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() + self.check_stock_items() self.check_for_payable_account() self.check_for_cost_center() @@ -53,6 +48,11 @@ class AssetRepair(Document): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) + def check_stock_items(self): + if self.stock_consumption: + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + def check_for_payable_account(self): if not self.payable_account: frappe.throw(_("Please enter Payable Account.")) From 1ac1cedfff947095879d032be1d74d433aca4c2d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 04:45:21 +0530 Subject: [PATCH 038/550] feat(Asset Repair): Decrease stock quantity if consumed during Asset Repair --- .../doctype/asset_repair/asset_repair.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ed022fd808..3b7587e61f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -37,21 +37,25 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_stock_items() + self.check_for_stock_items_and_warehouse() self.check_for_payable_account() self.check_for_cost_center() self.increase_asset_value() + if self.stock_consumption: + self.decrease_stock_quantity() self.make_gl_entries() def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) - def check_stock_items(self): + def check_for_stock_items_and_warehouse(self): if self.stock_consumption: if not self.stock_items: frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + if not self.warehouse: + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) def check_for_payable_account(self): if not self.payable_account: @@ -68,6 +72,22 @@ class AssetRepair(Document): asset_value += item.total_value frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + + def decrease_stock_quantity(self): + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Issue" + }) + + for stock_item in self.stock_items: + stock_entry.append('items', { + "s_warehouse": self.warehouse, + "item_code": stock_item.item, + "qty": stock_item.consumed_quantity + }) + + stock_entry.insert() + stock_entry.submit() def on_cancel(self): if self.payable_account: From ec72f8956f5c25dda1d19cd7691ac0a8349bae60 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:02:10 +0530 Subject: [PATCH 039/550] fix(Asset Repair): Revert to asset's previous status if repair is completed/cancelled --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3b7587e61f..975e3674a4 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -23,7 +23,8 @@ class AssetRepair(Document): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: - frappe.db.set_value('Asset', self.asset, 'status', 'Submitted') + asset = frappe.get_doc('Asset', self.asset) + asset.set_status() def set_total_value(self): for item in self.stock_items: From 68b78374b50ed09f83bb59103deeede2da416a6e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:03:56 +0530 Subject: [PATCH 040/550] fix(Asset): Make Manage button white --- erpnext/assets/doctype/asset/asset.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 2a57183a80..1e67ec816b 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -132,7 +132,6 @@ frappe.ui.form.on('Asset', { }, __("Manage")); } - frm.page.set_inner_btn_group_as_primary(__("Manage")); frm.trigger("setup_chart"); } From f7eebf0e782f1e3feb82df0feacadcd8306b938b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:08:33 +0530 Subject: [PATCH 041/550] feat(Asset Repair): Add 'Increase In Asset Life' field --- .../assets/doctype/asset_repair/asset_repair.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index ed7708c8df..26f1c6d7cc 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -30,6 +30,8 @@ "stock_consumption_details_section", "warehouse", "stock_items", + "asset_depreciation_details_section", + "increase_in_asset_life", "section_break_9", "description", "column_break_9", @@ -218,12 +220,22 @@ "fieldtype": "Link", "label": "Warehouse", "options": "Warehouse" + }, + { + "fieldname": "asset_depreciation_details_section", + "fieldtype": "Section Break", + "label": "Asset Depreciation Details" + }, + { + "fieldname": "increase_in_asset_life", + "fieldtype": "Int", + "label": "Increase In Asset Life" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 03:50:39.146322", + "modified": "2021-05-13 05:08:00.676616", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 2874d0853401d3b0570534d1c6dee34885ded1c0 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:22:35 +0530 Subject: [PATCH 042/550] fix(Asset Repair): Rearrange fields --- erpnext/assets/doctype/asset_repair/asset_repair.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 26f1c6d7cc..13f4610f8e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -13,19 +13,19 @@ "asset_name", "section_break_5", "failure_date", + "repair_status", "column_break_6", "completion_date", - "repair_status", "accounting_dimensions_section", "cost_center", "column_break_14", "project", "accounting_details", - "repair_cost", + "payable_account", "capitalize_repair_cost", "stock_consumption", "column_break_8", - "payable_account", + "repair_cost", "total_repair_cost", "stock_consumption_details_section", "warehouse", @@ -235,7 +235,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 05:08:00.676616", + "modified": "2021-05-13 05:11:42.550016", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 568369a5c20488d23cb61974f3d439e4603b756a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:23:21 +0530 Subject: [PATCH 043/550] feat(Asset Maintenance): Add stock_consumption checkbox --- .../doctype/asset_maintenance/asset_maintenance.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index c0c2566fe2..669f1959e5 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -12,6 +12,7 @@ "column_break_3", "item_code", "item_name", + "stock_consumption", "section_break_6", "maintenance_team", "column_break_9", @@ -100,10 +101,16 @@ "label": "Maintenance Tasks", "options": "Asset Maintenance Task", "reqd": 1 + }, + { + "default": "0", + "fieldname": "stock_consumption", + "fieldtype": "Check", + "label": "Stock Consumed During Maintenance" } ], "links": [], - "modified": "2020-05-28 20:28:32.993823", + "modified": "2021-05-13 05:20:45.809270", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", From 04a909bd994667a3eb1cdcd0c5223a214c7ece0d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:27:15 +0530 Subject: [PATCH 044/550] feat(Asset Maintenance): Add 'Stock Consumption Details' section --- .../asset_maintenance/asset_maintenance.js | 3 +++ .../asset_maintenance/asset_maintenance.json | 24 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 70b8654509..3830d1168c 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -30,7 +30,10 @@ frappe.ui.form.on('Asset Maintenance', { if(!frm.is_new()) { frm.trigger('make_dashboard'); } + + frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) }, + make_dashboard: (frm) => { if(!frm.is_new()) { frappe.call({ diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index 669f1959e5..da2fd75451 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -19,7 +19,10 @@ "maintenance_manager", "maintenance_manager_name", "section_break_8", - "asset_maintenance_tasks" + "asset_maintenance_tasks", + "stock_consumption_details_section", + "warehouse", + "stock_items" ], "fields": [ { @@ -107,10 +110,27 @@ "fieldname": "stock_consumption", "fieldtype": "Check", "label": "Stock Consumed During Maintenance" + }, + { + "fieldname": "stock_consumption_details_section", + "fieldtype": "Section Break", + "label": "Stock Consumption Details" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "options": "Stock Item" } ], "links": [], - "modified": "2021-05-13 05:20:45.809270", + "modified": "2021-05-13 05:24:58.480132", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", From d1f521701cf7a60b3e90ac81b1609432af8baca6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:33:19 +0530 Subject: [PATCH 045/550] feat(Asset Maintenance): Check if Warehouse and Stock Items were entered --- .../doctype/asset_maintenance/asset_maintenance.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index a506deec93..8bc7ea1b70 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -24,6 +24,16 @@ class AssetMaintenance(Document): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) self.sync_maintenance_tasks() + def on_submit(self): + self.check_for_stock_items_and_warehouse() + + def check_for_stock_items_and_warehouse(self): + if self.stock_consumption: + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Maintenance.")) + if not self.warehouse: + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) + def sync_maintenance_tasks(self): tasks_names = [] for task in self.get('asset_maintenance_tasks'): From 70bad470f7116c9ace21373aed205f8e7735c235 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:35:02 +0530 Subject: [PATCH 046/550] feat(Asset Maintenance): Increase Asset value if Stock Items were consumed --- .../assets/doctype/asset_maintenance/asset_maintenance.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 8bc7ea1b70..ecd55e38f2 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -26,6 +26,7 @@ class AssetMaintenance(Document): def on_submit(self): self.check_for_stock_items_and_warehouse() + self.increase_asset_value() def check_for_stock_items_and_warehouse(self): if self.stock_consumption: @@ -34,6 +35,13 @@ class AssetMaintenance(Document): if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) + def increase_asset_value(self): + asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + for item in self.stock_items: + asset_value += item.total_value + + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + def sync_maintenance_tasks(self): tasks_names = [] for task in self.get('asset_maintenance_tasks'): From a7bbaacfde57076d999f96577828698a41bad49a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:38:54 +0530 Subject: [PATCH 047/550] fix(Asset Repair): Always add value of consumed Stock Items to Asset value --- erpnext/assets/doctype/asset_repair/asset_repair.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 975e3674a4..3ac6f26f6c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -67,12 +67,13 @@ class AssetRepair(Document): frappe.throw(_("Please enter Cost Center.")) def increase_asset_value(self): - if self.capitalize_repair_cost: - asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + self.repair_cost - for item in self.stock_items: - asset_value += item.total_value + asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + for item in self.stock_items: + asset_value += item.total_value - frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + if self.capitalize_repair_cost: + asset_value += self.repair_cost + frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ From 15294d5543262d69a433861e67bf50042368d8f1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:40:56 +0530 Subject: [PATCH 048/550] feat(Asset Maintenance): Decrease stock quantity if consumed during Asset Maintenance --- .../asset_maintenance/asset_maintenance.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index ecd55e38f2..e3e654c398 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -19,14 +19,15 @@ class AssetMaintenance(Document): if not task.assign_to and self.docstatus == 0: throw(_("Row #{}: Please asign task to a member.").format(task.idx)) + if self.stock_consumption: + self.check_for_stock_items_and_warehouse() + self.increase_asset_value() + self.decrease_stock_quantity() + def on_update(self): for task in self.get('asset_maintenance_tasks'): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) - self.sync_maintenance_tasks() - - def on_submit(self): - self.check_for_stock_items_and_warehouse() - self.increase_asset_value() + self.sync_maintenance_tasks() def check_for_stock_items_and_warehouse(self): if self.stock_consumption: @@ -36,11 +37,27 @@ class AssetMaintenance(Document): frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) def increase_asset_value(self): - asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + asset_value = frappe.db.get_value('Asset', self.asset_name, 'asset_value') for item in self.stock_items: asset_value += item.total_value - frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + frappe.db.set_value('Asset', self.asset_name, 'asset_value', asset_value) + + def decrease_stock_quantity(self): + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Issue" + }) + + for stock_item in self.stock_items: + stock_entry.append('items', { + "s_warehouse": self.warehouse, + "item_code": stock_item.item, + "qty": stock_item.consumed_quantity + }) + + stock_entry.insert() + stock_entry.submit() def sync_maintenance_tasks(self): tasks_names = [] From d5d7cacd6712e8636f3102b0ede9796cb1217954 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:52:18 +0530 Subject: [PATCH 049/550] fix(Asset Repair): Improve code --- erpnext/assets/doctype/asset_repair/asset_repair.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3ac6f26f6c..8ff3b796d2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -38,12 +38,12 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_for_stock_items_and_warehouse() self.check_for_payable_account() self.check_for_cost_center() self.increase_asset_value() if self.stock_consumption: + self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() self.make_gl_entries() @@ -52,11 +52,10 @@ class AssetRepair(Document): frappe.throw(_("Please update Repair Status.")) def check_for_stock_items_and_warehouse(self): - if self.stock_consumption: - if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) - if not self.warehouse: - frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) + if not self.stock_items: + frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + if not self.warehouse: + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) def check_for_payable_account(self): if not self.payable_account: From 3ab852d4d00041fdd52250646d15b397d22001fa Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 05:56:43 +0530 Subject: [PATCH 050/550] fix(Asset Repair): Fetch 'Asset Name' when 'Asset' is selected --- erpnext/assets/doctype/asset_repair/asset_repair.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 13f4610f8e..968809c7d5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -143,6 +143,7 @@ "reqd": 1 }, { + "fetch_from": "asset.asset_name", "fieldname": "asset_name", "fieldtype": "Read Only", "label": "Asset Name" @@ -235,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 05:11:42.550016", + "modified": "2021-05-13 05:55:16.131448", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From a8037c1896f5bd4b0aa78c6f3e5c2409779affcf Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 23:07:19 +0530 Subject: [PATCH 051/550] fix(Asset Repair): Add Purchase Invoice --- erpnext/assets/doctype/asset_repair/asset_repair.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 968809c7d5..2c1552f026 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -22,11 +22,12 @@ "project", "accounting_details", "payable_account", - "capitalize_repair_cost", + "purchase_invoice", "stock_consumption", "column_break_8", "repair_cost", "total_repair_cost", + "capitalize_repair_cost", "stock_consumption_details_section", "warehouse", "stock_items", @@ -231,12 +232,18 @@ "fieldname": "increase_in_asset_life", "fieldtype": "Int", "label": "Increase In Asset Life" + }, + { + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "label": "Purchase Invoice", + "options": "Purchase Invoice" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 05:55:16.131448", + "modified": "2021-05-13 23:01:03.638835", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 66e6f01e40a0f9eaf6242d4eec7e97bc410e830f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 13 May 2021 23:17:17 +0530 Subject: [PATCH 052/550] fix(Asset Repair): Make Purchase Invoice mandatory if capitalize_repair_cost is checked --- erpnext/assets/doctype/asset_repair/asset_repair.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 8ff3b796d2..2807c0f5bd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -41,10 +41,13 @@ class AssetRepair(Document): self.check_for_payable_account() self.check_for_cost_center() - self.increase_asset_value() + if self.stock_consumption or self.capitalize_repair_cost: + self.increase_asset_value() if self.stock_consumption: self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() + if self.capitalize_repair_cost: + self.check_for_purchase_invoice() self.make_gl_entries() def check_repair_status(self): @@ -90,6 +93,10 @@ class AssetRepair(Document): stock_entry.insert() stock_entry.submit() + def check_for_purchase_invoice(self): + if not self.purchase_invoice: + frappe.throw(_("Please link Purchase Invoice.")) + def on_cancel(self): if self.payable_account: self.make_gl_entries(cancel=True) From 8a41f6354aa93dd1d366768a159e380a31766cdf Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 14 May 2021 02:35:13 +0530 Subject: [PATCH 053/550] fix(Asset Repair): Remove 'Payable Account' field --- .../assets/doctype/asset_repair/asset_repair.json | 15 ++++----------- .../assets/doctype/asset_repair/asset_repair.py | 8 +------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 2c1552f026..a2d962cd95 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -21,13 +21,12 @@ "column_break_14", "project", "accounting_details", - "payable_account", - "purchase_invoice", + "repair_cost", + "capitalize_repair_cost", "stock_consumption", "column_break_8", - "repair_cost", "total_repair_cost", - "capitalize_repair_cost", + "purchase_invoice", "stock_consumption_details_section", "warehouse", "stock_items", @@ -149,12 +148,6 @@ "fieldtype": "Read Only", "label": "Asset Name" }, - { - "fieldname": "payable_account", - "fieldtype": "Link", - "label": "Payable Account", - "options": "Account" - }, { "fieldname": "column_break_8", "fieldtype": "Column Break" @@ -243,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-13 23:01:03.638835", + "modified": "2021-05-14 02:31:57.226273", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 2807c0f5bd..5849c8ddd6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -38,7 +38,6 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_for_payable_account() self.check_for_cost_center() if self.stock_consumption or self.capitalize_repair_cost: @@ -60,10 +59,6 @@ class AssetRepair(Document): if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) - def check_for_payable_account(self): - if not self.payable_account: - frappe.throw(_("Please enter Payable Account.")) - def check_for_cost_center(self): if not self.cost_center: frappe.throw(_("Please enter Cost Center.")) @@ -98,8 +93,7 @@ class AssetRepair(Document): frappe.throw(_("Please link Purchase Invoice.")) def on_cancel(self): - if self.payable_account: - self.make_gl_entries(cancel=True) + self.make_gl_entries(cancel=True) def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: From 6bb920f25e9cf4e36d37fea774d478bb1117b594 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 14 May 2021 03:20:15 +0530 Subject: [PATCH 054/550] fix(Asset Repair): Only create GL entries if repair cost is capitalised --- .../doctype/asset_repair/asset_repair.py | 43 +++++-------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 5849c8ddd6..74d3114ea4 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -47,7 +47,7 @@ class AssetRepair(Document): self.decrease_stock_quantity() if self.capitalize_repair_cost: self.check_for_purchase_invoice() - self.make_gl_entries() + self.make_gl_entries() def check_repair_status(self): if self.repair_status == "Pending": @@ -104,14 +104,16 @@ class AssetRepair(Document): gl_entry = [] company = frappe.db.get_value('Asset', self.asset, 'company') repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') + fixed_asset_account = self.get_fixed_asset_account() + expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account gl_entry = frappe.get_doc({ "doctype": "GL Entry", - "account": self.payable_account, + "account": expense_account, "credit": self.total_repair_cost, "credit_in_account_currency": self.total_repair_cost, "against": repair_and_maintenance_account, - "voucher_type": self.doctype, + "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, "posting_date": getdate() @@ -119,44 +121,19 @@ class AssetRepair(Document): gl_entry.insert() gl_entry = frappe.get_doc({ "doctype": "GL Entry", - "account": repair_and_maintenance_account, + "account": fixed_asset_account, "debit": self.total_repair_cost, "debit_in_account_currency": self.total_repair_cost, - "against": self.payable_account, + "against": expense_account, "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate() + "posting_date": getdate(), + "against_voucher_type": "Purchase Invoice", + "against_voucher": self.purchase_invoice }) gl_entry.insert() - if self.capitalize_repair_cost: - fixed_asset_account = self.get_fixed_asset_account() - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": self.payable_account, - "credit": self.total_repair_cost, - "credit_in_account_currency": self.total_repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": "Asset", - "voucher_no": self.asset, - "cost_center": self.cost_center, - "posting_date": getdate() - }) - gl_entry.insert() - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": fixed_asset_account, - "debit": self.total_repair_cost, - "debit_in_account_currency": self.total_repair_cost, - "against": self.payable_account, - "voucher_type": "Asset", - "voucher_no": self.asset, - "cost_center": self.cost_center, - "posting_date": getdate() - }) - gl_entry.insert() - def get_fixed_asset_account(self): asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) company = frappe.db.get_value('Asset', self.asset, 'company') From 30fdebafa7dcb874b499505dc8e4515ae97fd58d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 10:12:28 +0530 Subject: [PATCH 055/550] feat(Asset): Modify depreciation schedule --- erpnext/assets/doctype/asset/asset.py | 19 ++++++++++++++----- .../doctype/asset_repair/asset_repair.js | 3 ++- .../doctype/asset_repair/asset_repair.py | 11 ++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 350220b897..f837c5e7c1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -171,15 +171,23 @@ class Asset(AccountsController): d.precision("rate_of_depreciation")) def make_depreciation_schedule(self): - if 'Manual' not in [d.depreciation_method for d in self.finance_books]: + if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: self.schedules = [] - if self.get("schedules") or not self.available_for_use_date: + if not self.available_for_use_date: return for d in self.get('finance_books'): self.validate_asset_finance_books(d) + start = 0 + for n in range (len(self.schedules)): + if not self.schedules[n].journal_entry: + print("*"*100) + del self.schedules[n:] + start = n + break + value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -192,9 +200,9 @@ class Asset(AccountsController): if has_pro_rata: number_of_pending_depreciations += 1 - + skip_row = False - for n in range(number_of_pending_depreciations): + for n in range(start, number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -350,11 +358,12 @@ class Asset(AccountsController): if d.finance_book_id not in finance_books: accumulated_depreciation = flt(self.opening_accumulated_depreciation) value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) - finance_books.append(d.finance_book_id) + finance_books.append(int(d.finance_book_id)) depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) value_after_depreciation -= flt(depreciation_amount) + # for the last row, if depreciation method = Straight Line if straight_line_idx and i == max(straight_line_idx) - 1: book = self.get('finance_books')[cint(d.finance_book_id) - 1] depreciation_amount += flt(value_after_depreciation - diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 9d06caeb98..3328e664fc 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -28,7 +28,8 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption) + frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption); + frm.toggle_display('asset_depreciation_details_section', frm.doc.capitalize_repair_cost); if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 74d3114ea4..95abbd3e94 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -48,6 +48,7 @@ class AssetRepair(Document): if self.capitalize_repair_cost: self.check_for_purchase_invoice() self.make_gl_entries() + self.modify_depreciation_schedule() def check_repair_status(self): if self.repair_status == "Pending": @@ -140,8 +141,16 @@ class AssetRepair(Document): for account in asset_category.accounts: if account.company_name == company: return account.fixed_asset_account + + def modify_depreciation_schedule(self): + if self.increase_in_asset_life: + asset = frappe.get_doc('Asset', self.asset) + asset.flags.ignore_validate_update_after_submit = True + for row in asset.finance_books: + row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + asset.prepare_depreciation_data() + asset.save() - @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) From 4277877883c120789f3867d20b88c7eabd7726f9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 10:40:16 +0530 Subject: [PATCH 056/550] feat(Asset Repair): Change visibilty of sections --- .../doctype/asset_repair/asset_repair.js | 28 +------------------ .../doctype/asset_repair/asset_repair.json | 7 +++-- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 3328e664fc..7633a595a2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,35 +2,9 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { - // setup: function(frm) { - // frm.add_fetch("company", "repair_and_maintenance_account", "payable_account"); - - // frm.set_query("payable_account", function() { - // return { - // filters: { - // "report_type": "Balance Sheet", - // "account_type": "Payable", - // "company": frm.doc.company, - // "is_group": 0 - // } - // }; - // }); - // }, - - // stock_items_add: function(frm){ - // var table = frm.doc.stock_items; - // for(var i in table) { - // if (table[i].valuation_rate == 0) { - // frm.set_value(table[i].total_value, (table[i].valuation_rate * table[i].consumed_quantity)) - // } - // } - // }, - refresh: function(frm) { frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - frm.toggle_display(['stock_consumption_details_section', 'total_repair_cost'], frm.doc.stock_consumption); - frm.toggle_display('asset_depreciation_details_section', frm.doc.capitalize_repair_cost); - + if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { frappe.route_options = { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index a2d962cd95..522f2874d9 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -201,11 +201,13 @@ "label": "Stock Consumed During Repair" }, { + "depends_on": "stock_consumption", "fieldname": "stock_consumption_details_section", "fieldtype": "Section Break", "label": "Stock Consumption Details" }, { + "depends_on": "stock_consumption", "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost" @@ -217,6 +219,7 @@ "options": "Warehouse" }, { + "depends_on": "capitalize_repair_cost", "fieldname": "asset_depreciation_details_section", "fieldtype": "Section Break", "label": "Asset Depreciation Details" @@ -224,7 +227,7 @@ { "fieldname": "increase_in_asset_life", "fieldtype": "Int", - "label": "Increase In Asset Life" + "label": "Increase In Asset Life(Months)" }, { "fieldname": "purchase_invoice", @@ -236,7 +239,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-14 02:31:57.226273", + "modified": "2021-05-21 10:37:35.002238", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 4b543de3293d0dfa1a2cbb51501ff7a863b64805 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 23:40:36 +0530 Subject: [PATCH 057/550] feat(Asset Repair): Modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation --- erpnext/assets/doctype/asset/asset.json | 16 ++++++++++++- erpnext/assets/doctype/asset/asset.py | 7 +++--- .../doctype/asset_repair/asset_repair.py | 24 +++++++++++++++++-- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 4960c7e709..8a0e3ad2a6 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -54,6 +54,8 @@ "next_depreciation_date", "section_break_14", "schedules", + "to_date", + "edit_dates", "insurance_details", "policy_number", "insurer", @@ -487,6 +489,18 @@ "fieldtype": "Currency", "label": "Asset Value", "read_only": 1 + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "hidden": 1, + "label": "To Date" + }, + { + "fieldname": "edit_dates", + "fieldtype": "Data", + "hidden": 1, + "label": "Edit Dates" } ], "idx": 72, @@ -509,7 +523,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-05-11 23:47:15.831720", + "modified": "2021-05-21 12:05:29.424083", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f837c5e7c1..2d012d672e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -228,11 +228,12 @@ class Asset(AccountsController): # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - to_date = add_months(self.available_for_use_date, - n * cint(d.frequency_of_depreciation)) + if not self.edit_dates: + self.to_date = add_months(self.available_for_use_date, + n * cint(d.frequency_of_depreciation)) depreciation_amount, days, months = get_pro_rata_amt(d, - depreciation_amount, schedule_date, to_date) + depreciation_amount, schedule_date, self.to_date) monthly_schedule_date = add_months(schedule_date, 1) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 95abbd3e94..8fd019febd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,9 +5,8 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate +from frappe.utils import time_diff_in_hours, getdate, add_days, date_diff, add_months, flt, cint from frappe.model.document import Document -from frappe.utils import flt from erpnext.accounts.general_ledger import make_gl_entries class AssetRepair(Document): @@ -148,8 +147,29 @@ class AssetRepair(Document): asset.flags.ignore_validate_update_after_submit = True for row in asset.finance_books: row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + + asset.edit_dates = "" + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date(asset, row, extra_months) + # fix depreciation amount + asset.prepare_depreciation_data() asset.save() + + # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation + def calculate_last_schedule_date(self, asset, row, extra_months): + asset.edit_dates = "Don't Edit" + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ + cint(asset.number_of_depreciations_booked) + last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + asset.to_date = add_months(last_schedule_date, extra_months) + schedule_date = add_months(row.depreciation_start_date, + number_of_pending_depreciations * cint(row.frequency_of_depreciation)) + + if asset.to_date > schedule_date: + row.total_number_of_depreciations += 1 + @frappe.whitelist() def get_downtime(failure_date, completion_date): From 28ca383534fad7734a82fe69502c1f6e1fcac72d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 21 May 2021 23:55:51 +0530 Subject: [PATCH 058/550] feat(Asset): Edit value_after_depreciation --- erpnext/assets/doctype/asset/asset.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2d012d672e..fb29ea0569 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -183,13 +183,12 @@ class Asset(AccountsController): start = 0 for n in range (len(self.schedules)): if not self.schedules[n].journal_entry: - print("*"*100) del self.schedules[n:] start = n break - value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + value_after_depreciation = (flt(self.asset_value) - + flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) d.value_after_depreciation = value_after_depreciation From 1d7dda2664d8966601c9520efd199a99e2b35497 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 22 May 2021 07:18:31 +0530 Subject: [PATCH 059/550] fix(Asset): Fix depreciation_amount calculation --- erpnext/assets/doctype/asset/asset.py | 37 +++++++++---------- .../doctype/asset_repair/asset_repair.py | 1 - 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index fb29ea0569..e61887a1f1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -205,8 +205,7 @@ class Asset(AccountsController): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = self.get_depreciation_amount(value_after_depreciation, - d.total_number_of_depreciations, d) + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: schedule_date = add_months(d.depreciation_start_date, @@ -377,24 +376,6 @@ class Asset(AccountsController): def get_value_after_depreciation(self, idx): return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation) - def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row): - precision = self.precision("gross_purchase_amount") - - if row.depreciation_method in ("Straight Line", "Manual"): - depreciation_left = (cint(row.total_number_of_depreciations) - cint(self.number_of_depreciations_booked)) - - if not depreciation_left: - frappe.msgprint(_("All the depreciations has been booked")) - depreciation_amount = flt(row.expected_value_after_useful_life) - return depreciation_amount - - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left - else: - depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100), precision) - - return depreciation_amount - def validate_expected_value_after_useful_life(self): for row in self.get('finance_books'): accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount @@ -791,3 +772,19 @@ def get_total_days(date, frequency): cint(frequency) * -1) return date_diff(date, period_start_date) + +@erpnext.allow_regional +def get_depreciation_amount(asset, depreciable_value, row): + depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) + + if row.depreciation_method in ("Straight Line", "Manual"): + if not asset.to_date: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / depreciation_left + else: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + else: + depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) + + return depreciation_amount diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 8fd019febd..9973afd80a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -152,7 +152,6 @@ class AssetRepair(Document): extra_months = self.increase_in_asset_life % row.frequency_of_depreciation if extra_months != 0: self.calculate_last_schedule_date(asset, row, extra_months) - # fix depreciation amount asset.prepare_depreciation_data() asset.save() From 03a6977a01490c35d9ad51bb7a94dd8fd08d2b2e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 22 May 2021 23:23:29 +0530 Subject: [PATCH 060/550] fix: Sider issues --- erpnext/assets/doctype/asset/asset.js | 2 +- erpnext/assets/doctype/asset/asset.py | 2 +- erpnext/assets/doctype/asset_maintenance/asset_maintenance.js | 2 +- erpnext/assets/doctype/asset_repair/asset_repair.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 1e67ec816b..922cc4a7b2 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -319,7 +319,7 @@ frappe.ui.form.on('Asset', { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } - }) + }); }, create_asset_adjustment: function(frm) { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e61887a1f1..1495a5fa5f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -181,7 +181,7 @@ class Asset(AccountsController): self.validate_asset_finance_books(d) start = 0 - for n in range (len(self.schedules)): + for n in range(len(self.schedules)): if not self.schedules[n].journal_entry: del self.schedules[n:] start = n diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 3830d1168c..19393b7e9d 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -31,7 +31,7 @@ frappe.ui.form.on('Asset Maintenance', { frm.trigger('make_dashboard'); } - frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption) + frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption); }, make_dashboard: (frm) => { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9973afd80a..3e81ba55b0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, add_days, date_diff, add_months, flt, cint +from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries From 48b1a82fa1d873470c6b5513742b104ef3f3573e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 29 May 2021 23:54:51 +0530 Subject: [PATCH 061/550] fix: Add accounts and templates for reverse charge --- erpnext/accounts/doctype/account/account.py | 6 - erpnext/accounts/utils.py | 2 +- erpnext/regional/india/setup.py | 68 +++++------ erpnext/setup/doctype/company/company.py | 2 +- .../setup_wizard/data/country_wise_tax.json | 115 +++++++++++++++++- .../setup_wizard/operations/company_setup.py | 23 ---- 6 files changed, 148 insertions(+), 68 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 645f49bcdc..1be2fbf5c8 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -28,12 +28,6 @@ class Account(NestedSet): from erpnext.accounts.utils import get_autoname_with_number self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) - def before_insert(self): - # Update Bank account name if conflicting with any other account - if frappe.flags.in_install and self.account_type == 'Bank': - if frappe.db.get_value('Account', {'account_name': self.account_name}): - self.account_name = self.account_name + '-1' - def validate(self): from erpnext.accounts.utils import validate_field_number if frappe.local.flags.allow_unverified_charts: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5a64e27ccb..121589930b 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -784,7 +784,7 @@ def get_children(doctype, parent, company, is_root=False): return acc def create_payment_gateway_account(gateway, payment_channel="Email"): - from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account + from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account company = frappe.db.get_value("Global Defaults", None, "default_company") if not company: diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 28f49e0a7e..d79ce64fb3 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -698,6 +698,7 @@ def setup_gst_settings(company): # Will only add default GST accounts if present input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] + rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM'] gst_settings = frappe.get_single('GST Settings') existing_account_list = [] @@ -706,45 +707,40 @@ def setup_gst_settings(company): existing_account_list.append(account.get(key)) gst_accounts = frappe._dict(frappe.get_all("Account", - {'company': company, 'name': ('like', "%GST%")}, ['account_name', 'name'], as_list=1)) + {'company': company, 'account_name': ('in', input_account_names + + output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1)) - all_input_account_exists = 0 - all_output_account_exists = 0 - - for account in input_account_names: - if not gst_accounts.get(account): - all_input_account_exists = 1 - - # Check if already added in GST Settings - if gst_accounts.get(account) in existing_account_list: - all_input_account_exists = 1 - - for account in output_account_names: - if not gst_accounts.get(account): - all_output_account_exists = 1 - - # Check if already added in GST Settings - if gst_accounts.get(account) in existing_account_list: - all_output_account_exists = 1 - - if not all_input_account_exists: - gst_settings.append('gst_accounts', { - 'company': company, - 'cgst_account': gst_accounts.get(input_account_names[0]), - 'sgst_account': gst_accounts.get(input_account_names[1]), - 'igst_account': gst_accounts.get(input_account_names[2]) - }) - - if not all_output_account_exists: - gst_settings.append('gst_accounts', { - 'company': company, - 'cgst_account': gst_accounts.get(output_account_names[0]), - 'sgst_account': gst_accounts.get(output_account_names[1]), - 'igst_account': gst_accounts.get(output_account_names[2]) - }) + add_accounts_in_gst_settings(company, input_account_names, gst_accounts, + existing_account_list, gst_settings) + add_accounts_in_gst_settings(company, output_account_names, gst_accounts, + existing_account_list, gst_settings) + add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts, + existing_account_list, gst_settings, is_reverse_charge=1) gst_settings.save() +def add_accounts_in_gst_settings(company, account_names, gst_accounts, + existing_account_list, gst_settings, is_reverse_charge=0): + accounts_not_added = 1 + + for account in account_names: + # Default Account Added does not exists + if not gst_accounts.get(account): + accounts_not_added = 0 + + # Check if already added in GST Settings + if gst_accounts.get(account) in existing_account_list: + accounts_not_added = 0 + + if accounts_not_added: + gst_settings.append('gst_accounts', { + 'company': company, + 'cgst_account': gst_accounts.get(account_names[0]), + 'sgst_account': gst_accounts.get(account_names[1]), + 'igst_account': gst_accounts.get(account_names[2]), + 'is_reverse_charge_account': is_reverse_charge + }) + def set_salary_components(docs): docs.extend([ {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', @@ -797,7 +793,7 @@ def set_tax_withholding_category(company): doc.append("rates", d.get('rates')[0]) doc.flags.ignore_permissions = True - doc.flags.ignore_validdate = True + doc.flags.ignore_validate = True doc.flags.ignore_mandatory = True doc.flags.ignore_links = True doc.save() diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 61d63a3f29..ff96c0b4c4 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -110,8 +110,8 @@ class Company(NestedSet): self.create_default_warehouses() if frappe.flags.country_change: - install_country_fixtures(self.name, self.country) self.create_default_tax_template() + install_country_fixtures(self.name, self.country) if not frappe.db.get_value("Department", {"company": self.name}): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index d21ef03e19..ee42a87320 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -830,7 +830,12 @@ "gst_state": "" }, { - "title": "Reverse Charge", + "title": "Reverse Charge In-State", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Reverse Charge Out-State", "is_inter_state": 0, "gst_state": "" }, @@ -879,6 +884,24 @@ "account_name": "Input Tax IGST", "tax_rate": 18.00 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 18.00 + } } ] }, @@ -920,6 +943,24 @@ "account_name": "Input Tax IGST", "tax_rate": 5.0 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 2.50 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 2.50 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 5.00 + } } ] }, @@ -961,6 +1002,24 @@ "account_name": "Input Tax IGST", "tax_rate": 12.0 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 6.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 6.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 12.00 + } } ] }, @@ -1002,6 +1061,24 @@ "account_name": "Input Tax IGST", "tax_rate": 28.0 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 14.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 14.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 28.00 + } } ] }, @@ -1110,6 +1187,42 @@ } ], "tax_category": "Out-State" + }, + { + "title": "Input GST RCM In-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 9.00, + "root_type": "Asset", + "account_type": "Tax" + } + }, + { + "account_head": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 9.00, + "root_type": "Asset", + "account_type": "Tax" + } + } + ], + "tax_category": "Reverse Charge In-State" + }, + { + "title": "Input GST RCM Out-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 18.00, + "root_type": "Asset", + "account_type": "Tax" + } + } + ], + "tax_category": "Reverse Charge Out-State" } ], "*": [ diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index 3f0bb14649..4edf9485dc 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -42,29 +42,6 @@ def enable_shopping_cart(args): 'quotation_series': "QTN-", }).insert() -def create_bank_account(args): - if args.get("bank_account"): - company_name = args.get('company_name') - bank_account_group = frappe.db.get_value("Account", - {"account_type": "Bank", "is_group": 1, "root_type": "Asset", - "company": company_name}) - if bank_account_group: - bank_account = frappe.get_doc({ - "doctype": "Account", - 'account_name': args.get("bank_account"), - 'parent_account': bank_account_group, - 'is_group':0, - 'company': company_name, - "account_type": "Bank", - }) - try: - return bank_account.insert() - except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account"))) - except frappe.DuplicateEntryError: - # bank account same as a CoA entry - pass - def create_email_digest(): from frappe.utils.user import get_system_managers system_managers = get_system_managers(only_name=True) From 6432aaa07abc3abcb339bf73a096ce93b78269d0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 12 May 2021 16:34:09 +0530 Subject: [PATCH 062/550] feat: added reports to check incorrect qty and valuation for serialized items --- .../__init__.py | 0 ...incorrect_balance_qty_after_transaction.js | 27 ++++ ...correct_balance_qty_after_transaction.json | 32 ++++ ...incorrect_balance_qty_after_transaction.py | 111 +++++++++++++ .../incorrect_serial_no_valuation/__init__.py | 0 .../incorrect_serial_no_valuation.js | 35 +++++ .../incorrect_serial_no_valuation.json | 36 +++++ .../incorrect_serial_no_valuation.py | 148 ++++++++++++++++++ erpnext/stock/workspace/stock/stock.json | 38 ++++- 9 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json create mode 100644 erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/__init__.py create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js new file mode 100644 index 0000000000..bf11277d9c --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js @@ -0,0 +1,27 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Balance Qty After Transaction"] = { + "filters": [ + { + label: __("Company"), + fieldtype: "Link", + fieldname: "company", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __('Item Code'), + fieldtype: 'Link', + fieldname: 'item_code', + options: 'Item' + }, + { + label: __('Warehouse'), + fieldtype: 'Link', + fieldname: 'warehouse' + } + ] +}; diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json new file mode 100644 index 0000000000..a5815bcca4 --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-05-12 16:47:58.717853", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-05-12 16:48:28.347575", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Balance Qty After Transaction", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Balance Qty After Transaction", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + }, + { + "role": "Purchase User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py new file mode 100644 index 0000000000..cf174c9368 --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py @@ -0,0 +1,111 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from six import iteritems +from frappe.utils import flt + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters): + data = get_stock_ledger_entries(filters) + itewise_balance_qty = {} + + for row in data: + key = (row.item_code, row.warehouse) + itewise_balance_qty.setdefault(key, []).append(row) + + res = validate_data(itewise_balance_qty) + return res + +def validate_data(itewise_balance_qty): + res = [] + for key, data in iteritems(itewise_balance_qty): + row = get_incorrect_data(data) + if row: + res.append(row) + res.append({}) + + return res + +def get_incorrect_data(data): + balance_qty = 0.0 + for row in data: + balance_qty += row.actual_qty + if row.voucher_type == "Stock Reconciliation" and not row.batch_no: + balance_qty = flt(row.qty_after_transaction) + + row.expected_balance_qty = balance_qty + if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5: + row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) + return row + +def get_stock_ledger_entries(report_filters): + filters = {} + fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty', + 'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no'] + + for field in ['warehouse', 'item_code', 'company']: + if report_filters.get(field): + filters[field] = report_filters.get(field) + + return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, + order_by = 'timestamp(posting_date, posting_time) asc, creation asc') + +def get_columns(): + return [{ + 'label': _('Id'), + 'fieldtype': 'Link', + 'fieldname': 'name', + 'options': 'Stock Ledger Entry', + 'width': 120 + }, { + 'label': _('Posting Date'), + 'fieldtype': 'Date', + 'fieldname': 'posting_date', + 'width': 110 + }, { + 'label': _('Voucher Type'), + 'fieldtype': 'Link', + 'fieldname': 'voucher_type', + 'options': 'DocType', + 'width': 120 + }, { + 'label': _('Voucher No'), + 'fieldtype': 'Dynamic Link', + 'fieldname': 'voucher_no', + 'options': 'voucher_type', + 'width': 120 + }, { + 'label': _('Item Code'), + 'fieldtype': 'Link', + 'fieldname': 'item_code', + 'options': 'Item', + 'width': 120 + }, { + 'label': _('Warehouse'), + 'fieldtype': 'Link', + 'fieldname': 'warehouse', + 'options': 'Warehouse', + 'width': 120 + }, { + 'label': _('Expected Balance Qty'), + 'fieldtype': 'Float', + 'fieldname': 'expected_balance_qty', + 'width': 170 + }, { + 'label': _('Actual Balance Qty'), + 'fieldtype': 'Float', + 'fieldname': 'qty_after_transaction', + 'width': 150 + }, { + 'label': _('Difference'), + 'fieldtype': 'Float', + 'fieldname': 'differnce', + 'width': 110 + }] \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js new file mode 100644 index 0000000000..c62d48081c --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js @@ -0,0 +1,35 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Serial No Valuation"] = { + "filters": [ + { + label: __('Item Code'), + fieldtype: 'Link', + fieldname: 'item_code', + options: 'Item', + get_query: function() { + return { + filters: { + 'has_serial_no': 1 + } + } + } + }, + { + label: __('From Date'), + fieldtype: 'Date', + fieldname: 'from_date', + reqd: 1, + default: frappe.defaults.get_user_default("year_start_date") + }, + { + label: __('To Date'), + fieldtype: 'Date', + fieldname: 'to_date', + reqd: 1, + default: frappe.defaults.get_user_default("year_end_date") + } + ] +}; diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json new file mode 100644 index 0000000000..cc384a5bd0 --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json @@ -0,0 +1,36 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-05-13 13:07:00.767845", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2021-05-13 13:07:00.767845", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Serial No Valuation", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Serial No Valuation", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Accounts User" + }, + { + "role": "Stock Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py new file mode 100644 index 0000000000..e54cf4c66c --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -0,0 +1,148 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +import copy +from frappe import _ +from six import iteritems +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters): + data = get_stock_ledger_entries(filters) + serial_nos_data = prepare_serial_nos(data) + data = get_incorrect_serial_nos(serial_nos_data) + + return data + +def prepare_serial_nos(data): + serial_no_wise_data = {} + for row in data: + if not row.serial_nos: + continue + + for serial_no in get_serial_nos(row.serial_nos): + sle = copy.deepcopy(row) + sle.serial_no = serial_no + sle.qty = 1 if sle.actual_qty > 0 else -1 + sle.valuation_rate = sle.valuation_rate if sle.actual_qty > 0 else sle.valuation_rate * -1 + serial_no_wise_data.setdefault(serial_no, []).append(sle) + + return serial_no_wise_data + +def get_incorrect_serial_nos(serial_nos_data): + result = [] + + total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))}) + + for serial_no, data in iteritems(serial_nos_data): + total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))}) + + if check_incorrect_serial_data(data, total_dict): + result.extend(data) + + total_value.qty += total_dict.qty + total_value.valuation_rate += total_dict.valuation_rate + + result.append(total_dict) + result.append({}) + + result.append(total_value) + + return result + +def check_incorrect_serial_data(data, total_dict): + incorrect_data = False + for row in data: + total_dict.qty += row.qty + total_dict.valuation_rate += row.valuation_rate + + if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0): + incorrect_data = True + + return incorrect_data + +def get_stock_ledger_entries(report_filters): + fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty', + 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate'] + + filters = {'serial_no': ("is", "set")} + + if report_filters.get('item_code'): + filters['item_code'] = report_filters.get('item_code') + + if report_filters.get('from_date') and report_filters.get('to_date'): + filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')]) + + return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, + order_by = 'timestamp(posting_date, posting_time) asc, creation asc') + +def get_columns(): + return [{ + 'label': _('Company'), + 'fieldtype': 'Link', + 'fieldname': 'company', + 'options': 'Company', + 'width': 120 + }, { + 'label': _('Id'), + 'fieldtype': 'Link', + 'fieldname': 'name', + 'options': 'Stock Ledger Entry', + 'width': 120 + }, { + 'label': _('Posting Date'), + 'fieldtype': 'Date', + 'fieldname': 'posting_date', + 'width': 90 + }, { + 'label': _('Posting Time'), + 'fieldtype': 'Time', + 'fieldname': 'posting_time', + 'width': 90 + }, { + 'label': _('Voucher Type'), + 'fieldtype': 'Link', + 'fieldname': 'voucher_type', + 'options': 'DocType', + 'width': 100 + }, { + 'label': _('Voucher No'), + 'fieldtype': 'Dynamic Link', + 'fieldname': 'voucher_no', + 'options': 'voucher_type', + 'width': 110 + }, { + 'label': _('Item Code'), + 'fieldtype': 'Link', + 'fieldname': 'item_code', + 'options': 'Item', + 'width': 120 + }, { + 'label': _('Warehouse'), + 'fieldtype': 'Link', + 'fieldname': 'warehouse', + 'options': 'Warehouse', + 'width': 120 + }, { + 'label': _('Serial No'), + 'fieldtype': 'Link', + 'fieldname': 'serial_no', + 'options': 'Serial No', + 'width': 100 + }, { + 'label': _('Qty'), + 'fieldtype': 'Float', + 'fieldname': 'qty', + 'width': 80 + }, { + 'label': _('Valuation Rate (In / Out)'), + 'fieldtype': 'Currency', + 'fieldname': 'valuation_rate', + 'width': 110 + }] \ No newline at end of file diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index 3221dc4365..529ce8eb61 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -15,6 +15,7 @@ "hide_custom": 0, "icon": "stock", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Stock", "links": [ @@ -653,9 +654,44 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Incorrect Data Report", + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Incorrect Serial No Qty and Valuation", + "link_to": "Incorrect Serial No Valuation", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Incorrect Balance Qty After Transaction", + "link_to": "Incorrect Balance Qty After Transaction", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Stock and Account Value Comparison", + "link_to": "Stock and Account Value Comparison", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], - "modified": "2020-12-01 13:38:36.282890", + "modified": "2021-05-13 13:10:24.914983", "modified_by": "Administrator", "module": "Stock", "name": "Stock", From c6dcc9d94a19fef68ebaf792063beb0fd19c8c3a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Jun 2021 13:13:04 +0530 Subject: [PATCH 063/550] fix(India): Taxable value for invoices with additional discount --- erpnext/regional/india/e_invoice/utils.py | 30 ++++++----------------- erpnext/regional/india/utils.py | 8 ++---- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 843fb012b9..95b4e16afd 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -38,7 +38,7 @@ def validate_eligibility(doc): einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): return False - + invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') @@ -135,7 +135,7 @@ def validate_address_fields(address, is_shipping_address): def get_party_details(address_name, is_shipping_address=False): addr = frappe.get_doc('Address', address_name) - + validate_address_fields(addr, is_shipping_address) if addr.gst_state_number == 97: @@ -188,11 +188,6 @@ def get_item_list(invoice): item.qty = abs(item.qty) - if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - item.discount_amount = abs(item.base_amount - item.base_net_amount) - else: - item.discount_amount = 0 - item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) item.gross_amount = abs(item.taxable_value) + item.discount_amount item.taxable_value = abs(item.taxable_value) @@ -254,18 +249,8 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - - if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - # Discount already applied on net total which means on items - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - invoice_value_details.invoice_discount_amt = 0 - elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount: - invoice_value_details.invoice_discount_amt = invoice.base_discount_amount - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - else: - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - # since tax already considers discount amount - invoice_value_details.invoice_discount_amt = 0 + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.invoice_discount_amt = 0 invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) @@ -287,8 +272,7 @@ def update_invoice_taxes(invoice, invoice_value_details): considered_rows = [] for t in invoice.taxes: - tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \ - else t.base_tax_amount_after_discount_amount + tax_amount = t.base_tax_amount_after_discount_amount if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: # using after discount amt since item also uses after discount amt for cess calc @@ -995,7 +979,7 @@ class GSPConnector(): self.invoice.failure_description = self.get_failure_message(errors) if errors else "" self.update_invoice() frappe.db.commit() - + def get_failure_message(self, errors): if isinstance(errors, list): errors = ', '.join(errors) @@ -1052,7 +1036,7 @@ def generate_einvoices(docnames): _('{} e-invoices generated successfully').format(success), title=_('Bulk E-Invoice Generation Complete') ) - + else: enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fc227defbf..ea61502099 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -817,12 +817,8 @@ def update_taxable_values(doc, method): considered_rows.append(prev_row_id) for item in doc.get('items'): - if doc.apply_discount_on == 'Grand Total' and doc.discount_amount: - proportionate_value = item.base_amount if doc.base_total else item.qty - total_value = doc.base_total if doc.base_total else doc.total_qty - else: - proportionate_value = item.base_net_amount if doc.base_net_total else item.qty - total_value = doc.base_net_total if doc.base_net_total else doc.total_qty + proportionate_value = item.base_net_amount if doc.base_net_total else item.qty + total_value = doc.base_net_total if doc.base_net_total else doc.total_qty applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), item.precision('taxable_value'))) From b3ed807b70d5bd1b1b94442fd0c4f1d774846d9d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Jun 2021 13:26:21 +0530 Subject: [PATCH 064/550] fix: Regional settings setup --- erpnext/regional/india/setup.py | 3 +-- erpnext/setup/doctype/company/company.py | 2 +- erpnext/setup/setup_wizard/operations/taxes_setup.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index d79ce64fb3..fa9f1b7e9e 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -15,7 +15,6 @@ def setup(company=None, patch=True): setup_company_independent_fixtures(patch=patch) if not patch: make_fixtures(company) - setup_gst_settings(company) # TODO: for all countries def setup_company_independent_fixtures(patch=False): @@ -694,7 +693,7 @@ def make_fixtures(company=None): # create records for Tax Withholding Category set_tax_withholding_category(company) -def setup_gst_settings(company): +def update_regional_tax_settings(country, company): # Will only add default GST accounts if present input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index ff96c0b4c4..61d63a3f29 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -110,8 +110,8 @@ class Company(NestedSet): self.create_default_warehouses() if frappe.flags.country_change: - self.create_default_tax_template() install_country_fixtures(self.name, self.country) + self.create_default_tax_template() if not frappe.db.get_value("Department", {"company": self.name}): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index a644da9292..6ac561223a 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -24,6 +24,7 @@ def setup_taxes_and_charges(company_name: str, country: str): country_wise_tax = simple_to_detailed(country_wise_tax) from_detailed_data(company_name, country_wise_tax.get('chart_of_accounts')) + update_regional_tax_settings(country, company_name) def simple_to_detailed(templates): @@ -97,6 +98,17 @@ def from_detailed_data(company_name, data): make_item_tax_template(company_name, template) +def update_regional_tax_settings(country, company): + path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) + if os.path.exists(path.encode("utf-8")): + try: + module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country)) + frappe.get_attr(module_name)(country, company) + except Exception as e: + # Log error and ignore if failed to setup regional tax settings + frappe.log_error() + pass + def make_taxes_and_charges_template(company_name, doctype, template): template['company'] = company_name template['doctype'] = doctype From 7c7c084159b68f99927cc28b4b3b88fa8c415b80 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Jun 2021 14:12:28 +0530 Subject: [PATCH 065/550] fix: Check for tax category --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 4c49609039..9a830d4f2e 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -176,7 +176,7 @@ def make_item_tax_template(company_name, template): def make_tax_category(tax_category): """ Make tax category based on title if not already created """ doctype = 'Tax Category' - if not frappe.db.exists(doctype, tax_category): + if not frappe.db.exists(doctype, tax_category['title']): tax_category['doctype'] = doctype doc = frappe.get_doc(tax_category) doc.flags.ignore_links = True From 960840dc3ad9c4f12268dc06f3f00fcc7fb4ace8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 01:53:13 +0530 Subject: [PATCH 066/550] fix(Asset Repair): Set completion_date --- .../assets/doctype/asset_repair/asset_repair.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3e81ba55b0..ad0792e3ea 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,18 +5,19 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint +from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, cint from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries class AssetRepair(Document): def validate(self): if self.repair_status == "Completed" and not self.completion_date: - frappe.throw(_("Please select Completion Date for Completed Repair")) + self.completion_date = nowdate() self.update_status() - self.set_total_value() # change later - self.calculate_total_repair_cost() + if self.stock_consumption: + self.set_total_value() # change later + self.calculate_total_repair_cost() def update_status(self): if self.repair_status == 'Pending': @@ -31,9 +32,8 @@ class AssetRepair(Document): def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost - if self.stock_consumption: - for item in self.stock_items: - self.total_repair_cost += item.total_value + for item in self.stock_items: + self.total_repair_cost += item.total_value def on_submit(self): self.check_repair_status() From 2ceeb8138d03076e7192b9a97bd14741976ce5fc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 01:54:44 +0530 Subject: [PATCH 067/550] fix(Asset Repair): Set company when creating Stock Entry --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ad0792e3ea..c39b20c50d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -75,7 +75,8 @@ class AssetRepair(Document): def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", - "stock_entry_type": "Material Issue" + "stock_entry_type": "Material Issue", + "company": frappe.get_value('Asset', self.asset, "company") }) for stock_item in self.stock_items: From 9fb47795c4b3d03ee36273959b4b844dacb35bb7 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 04:55:49 +0530 Subject: [PATCH 068/550] fix(Asset Repair): Only modify depreciation schedule if calculate_depreciation is checked --- erpnext/assets/doctype/asset_repair/asset_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index c39b20c50d..fa918b7efd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -47,7 +47,8 @@ class AssetRepair(Document): if self.capitalize_repair_cost: self.check_for_purchase_invoice() self.make_gl_entries() - self.modify_depreciation_schedule() + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation'): + self.modify_depreciation_schedule() def check_repair_status(self): if self.repair_status == "Pending": From 034f7bde336a8bfa69787d382634c7848511584d Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 21:07:07 +0530 Subject: [PATCH 069/550] fix(Asset Repair): Remove unnecessary condition --- erpnext/assets/doctype/asset_repair/asset_repair.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index fa918b7efd..80c9f09e14 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -15,9 +15,8 @@ class AssetRepair(Document): self.completion_date = nowdate() self.update_status() - if self.stock_consumption: - self.set_total_value() # change later - self.calculate_total_repair_cost() + self.set_total_value() # change later + self.calculate_total_repair_cost() def update_status(self): if self.repair_status == 'Pending': @@ -72,7 +71,7 @@ class AssetRepair(Document): if self.capitalize_repair_cost: asset_value += self.repair_cost frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) - + def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", From aa9bbe51bd0205918f75cc035efb9c07b055ca80 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 21:28:58 +0530 Subject: [PATCH 070/550] fix(Asset Repair): Add Company in GL Entries --- erpnext/assets/doctype/asset_repair/asset_repair.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 80c9f09e14..0b4612e754 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -117,7 +117,8 @@ class AssetRepair(Document): "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, - "posting_date": getdate() + "posting_date": getdate(), + "company": company }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -131,7 +132,8 @@ class AssetRepair(Document): "cost_center": self.cost_center, "posting_date": getdate(), "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice + "against_voucher": self.purchase_invoice, + "company": company }) gl_entry.insert() From b55649f2ecf1b4b0f4121fe0d7ad986517d021b8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 22:28:30 +0530 Subject: [PATCH 071/550] fix(Asset): Add depreciation schedule details in create_asset() --- erpnext/assets/doctype/asset/test_asset.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a0d76031fc..568a41039c 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -660,7 +660,7 @@ def create_asset(**args): "item_code": args.item_code or "Macbook Pro", "company": args.company or"_Test Company", "purchase_date": "2015-01-01", - "calculate_depreciation": 0, + "calculate_depreciation": args.calculate_depreciation or 0, "gross_purchase_amount": 100000, "purchase_receipt_amount": 100000, "expected_value_after_useful_life": 10000, @@ -671,6 +671,13 @@ def create_asset(**args): "is_existing_asset": args.is_existing_asset or 0 }) + if asset.calculate_depreciation: + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 5 + }) + try: asset.save() except frappe.DuplicateEntryError: From 12d9e3b1e6e04dbc2eae8f2f7a588fbafbdfe59b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 3 Jun 2021 22:28:57 +0530 Subject: [PATCH 072/550] fix(Asset Repair): Add tests --- .../doctype/asset_repair/asset_repair.py | 1 - .../doctype/asset_repair/test_asset_repair.py | 166 +++++++++++++++++- 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 0b4612e754..8724d0a125 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -172,7 +172,6 @@ class AssetRepair(Document): if asset.to_date > schedule_date: row.total_number_of_depreciations += 1 - @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 3d325a9683..9c9dd44971 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -2,8 +2,170 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals - +import frappe +from frappe.utils import nowdate, flt import unittest +from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company class TestAssetRepair(unittest.TestCase): - pass + def setUp(self): + set_depreciation_settings_in_company() + create_asset_data() + frappe.db.sql("delete from `tabTax Rule`") + + def test_completion_date(self): + asset_repair = create_asset_repair() + asset_repair.repair_status = "Completed" + asset_repair.save() + self.assertTrue(asset_repair.completion_date) + + def test_update_status(self): + asset = create_asset() + initial_status = asset.status + asset_repair = create_asset_repair(asset = asset) + + if asset_repair.repair_status == "Pending": + asset.reload() + self.assertEqual(asset.status, "Out of Order") + + asset_repair.repair_status = "Completed" + asset_repair.save() + asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status") + self.assertEqual(asset_status, initial_status) + + def test_stock_item_total_value(self): + asset_repair = create_asset_repair(stock_consumption = 1) + + for item in asset_repair.stock_items: + total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + self.assertEqual(item.total_value, total_value) + + def test_total_repair_cost(self): + asset_repair = create_asset_repair(stock_consumption = 1) + + total_repair_cost = asset_repair.repair_cost + self.assertEqual(total_repair_cost, asset_repair.repair_cost) + for item in asset_repair.stock_items: + total_repair_cost += item.total_value + + self.assertEqual(total_repair_cost, asset_repair.total_repair_cost) + + def test_repair_status_after_submit(self): + asset_repair = create_asset_repair(submit = 1) + self.assertNotEqual(asset_repair.repair_status, "Pending") + + def test_stock_items(self): + asset_repair = create_asset_repair(stock_consumption = 1) + self.assertTrue(asset_repair.stock_consumption) + self.assertTrue(asset_repair.stock_items) + + def test_warehouse(self): + asset_repair = create_asset_repair(stock_consumption = 1) + self.assertTrue(asset_repair.stock_consumption) + self.assertTrue(asset_repair.warehouse) + + def test_decrease_stock_quantity(self): + asset_repair = create_asset_repair(stock_consumption = 1, submit = 1) + stock_entry = frappe.get_last_doc('Stock Entry') + + self.assertEqual(stock_entry.stock_entry_type, "Material Issue") + self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) + self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item) + self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) + + def test_increase_in_asset_value_due_to_stock_consumption(self): + asset = create_asset() + initial_asset_value = asset.asset_value + asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1) + asset.reload() + + increase_in_asset_value = asset.asset_value - initial_asset_value + self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) + + def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): + asset = create_asset() + initial_asset_value = asset.asset_value + asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + asset.reload() + + increase_in_asset_value = asset.asset_value - initial_asset_value + self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) + + def test_purchase_invoice(self): + asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) + self.assertTrue(asset_repair.purchase_invoice) + + def test_gl_entries(self): + asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1) + gl_entry = frappe.get_last_doc('GL Entry') + self.assertEqual(asset_repair.name, gl_entry.voucher_no) + + def test_increase_in_asset_life(self): + asset = create_asset(calculate_depreciation = 1) + initial_num_of_depreciations = num_of_depreciations(asset) + create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) + asset.reload() + self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) + +def num_of_depreciations(asset): + return asset.finance_books[0].total_number_of_depreciations + +def create_asset_repair(**args): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + + args = frappe._dict(args) + + if args.asset: + asset = args.asset + else: + asset = create_asset(is_existing_asset = 1) + asset_repair = frappe.new_doc("Asset Repair") + asset_repair.update({ + "asset": asset.name, + "asset_name": asset.asset_name, + "failure_date": nowdate(), + "description": "Test Description", + "repair_cost": 0 + }) + + if args.stock_consumption: + asset_repair.stock_consumption = 1 + asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company) + asset_repair.append("stock_items", { + "item": args.item or args.item_code or "_Test Item", + "valuation_rate": args.rate if args.get("rate") is not None else 100, + "consumed_quantity": args.qty or 1 + }) + + try: + asset_repair.save() + except frappe.DuplicateEntryError: + pass + + if args.submit: + asset_repair.repair_status = "Completed" + asset_repair.cost_center = "_Test Cost Center - _TC" + + if args.stock_consumption: + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Receipt", + "company": asset.company + }) + stock_entry.append('items', { + "t_warehouse": asset_repair.warehouse, + "item_code": asset_repair.stock_items[0].item, + "qty": asset_repair.stock_items[0].consumed_quantity + }) + stock_entry.submit() + + if args.capitalize_repair_cost: + asset_repair.capitalize_repair_cost = 1 + asset_repair.repair_cost = 1000 + if asset.calculate_depreciation: + asset_repair.increase_in_asset_life = 12 + asset_repair.purchase_invoice = make_purchase_invoice().name + + asset_repair.submit() + return asset_repair \ No newline at end of file From 48036b4bab25fcecf2728decc78ae2c598f54c0e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 4 Jun 2021 09:54:34 +0530 Subject: [PATCH 073/550] fix: invoices can alter profit and loss of a closed year --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 948c51364e..11465b711e 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -121,8 +121,7 @@ class GLEntry(Document): def check_pl_account(self): if self.is_opening=='Yes' and \ - frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \ - self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']: + frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss": frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") .format(self.voucher_type, self.voucher_no, self.account)) From edecd5b0c686f653454fa3d85fa87e058ad81f9b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Jun 2021 12:04:30 +0530 Subject: [PATCH 074/550] fix: Update einvoice json test --- .../sales_invoice/test_sales_invoice.py | 98 ++++++++----------- 1 file changed, 41 insertions(+), 57 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index df6d483904..b35686f4f0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1899,69 +1899,53 @@ class TestSalesInvoice(unittest.TestCase): frappe.flags.country = country def test_einvoice_json(self): - from erpnext.regional.india.e_invoice.utils import make_einvoice + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals - si = make_sales_invoice_for_ewaybill() - si.naming_series = 'INV-2020-.#####' - si.items = [] - si.append("items", { - "item_code": "_Test Item", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 2000, - "rate": 12, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) - si.append("items", { - "item_code": "_Test Item 2", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 420, - "rate": 15, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) + si = get_sales_invoice_for_e_invoice() si.discount_amount = 100 si.save() einvoice = make_einvoice(si) - - total_item_ass_value = 0 - total_item_cgst_value = 0 - total_item_sgst_value = 0 - total_item_igst_value = 0 - total_item_value = 0 - - for item in einvoice['ItemList']: - total_item_ass_value += item['AssAmt'] - total_item_cgst_value += item['CgstAmt'] - total_item_sgst_value += item['SgstAmt'] - total_item_igst_value += item['IgstAmt'] - total_item_value += item['TotItemVal'] - - self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount']) - self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt']) - - value_details = einvoice['ValDtls'] - - self.assertEqual(einvoice['Version'], '1.1') - self.assertEqual(value_details['AssVal'], total_item_ass_value) - self.assertEqual(value_details['CgstVal'], total_item_cgst_value) - self.assertEqual(value_details['SgstVal'], total_item_sgst_value) - self.assertEqual(value_details['IgstVal'], total_item_igst_value) - - calculated_invoice_value = \ - value_details['AssVal'] + value_details['CgstVal'] \ - + value_details['SgstVal'] + value_details['IgstVal'] \ - + value_details['OthChrg'] - value_details['Discount'] - - self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1) - - self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) + validate_totals(einvoice) + + si.apply_discount_on = 'Net Total' + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + + [d.set('included_in_print_rate', 1) for d in si.taxes] + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + +def get_sales_invoice_for_e_invoice(): + si = make_sales_invoice_for_ewaybill() + si.naming_series = 'INV-2020-.#####' + si.items = [] + si.append("items", { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2000, + "rate": 12, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + + si.append("items", { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 420, + "rate": 15, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + + return si def make_test_address_for_ewaybill(): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): From 88176e65e46c43f1a5a6e88f3d08a2bcb6e3e5fb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Jun 2021 17:31:28 +0530 Subject: [PATCH 075/550] fix: Check for gst_hsn_code --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5a53d49399..11ebef724c 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -194,7 +194,7 @@ def get_item_list(invoice): item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None - item.is_service_item = 'Y' if item.gst_hsn_code[:2] == "99" else 'N' + item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N' item.serial_no = "" item = update_item_taxes(invoice, item) From 687ad9b9421074a073d3087e3c266a72473bb7c7 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Jun 2021 21:18:19 +0530 Subject: [PATCH 076/550] fix: Report Raw Materials to be Transferred --- ...tracted_raw_materials_to_be_transferred.py | 118 ++++++------------ 1 file changed, 39 insertions(+), 79 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index de2ae8fc73..5a0381b017 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -9,10 +9,10 @@ def execute(filters=None): if filters.from_date >= filters.to_date: frappe.msgprint(_("To Date must be greater than From Date")) - data = [] columns = get_columns() - get_data(data , filters) - return columns, data + data = get_data(filters) + + return columns, data or [] def get_columns(): return [ @@ -21,13 +21,12 @@ def get_columns(): "fieldtype": "Link", "fieldname": "purchase_order", "options": "Purchase Order", - "width": 150 + "width": 200 }, { "label": _("Date"), "fieldtype": "Date", "fieldname": "date", - "hidden": 1, "width": 150 }, { @@ -41,97 +40,58 @@ def get_columns(): "label": _("Item Code"), "fieldtype": "Data", "fieldname": "rm_item_code", - "width": 100 + "width": 150 }, { "label": _("Required Quantity"), "fieldtype": "Float", - "fieldname": "r_qty", - "width": 100 + "fieldname": "reqd_qty", + "width": 150 }, { "label": _("Transferred Quantity"), "fieldtype": "Float", - "fieldname": "t_qty", - "width": 100 + "fieldname": "transferred_qty", + "width": 200 }, { "label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", - "width": 100 + "width": 150 } ] -def get_data(data, filters): - po = get_po(filters) - po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po])) +def get_data(filters): + po_rm_item_details = get_po_items_to_supply(filters) - sub_items = get_purchase_order_item_supplied([v.name for v in po]) + data = [] + for row in po_rm_item_details: + transferred_qty = row.get("transferred_qty") or 0 + if transferred_qty < row.get("reqd_qty", 0): + pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) + row.p_qty = pending_qty if pending_qty > 0 else 0 + data.append(row) - for order in po: - for item in sub_items: - if order.name == item.parent and order.name in po_transferred_qty_map and \ - item.required_qty != po_transferred_qty_map.get(order.name).get(item.rm_item_code): - transferred_qty = po_transferred_qty_map.get(order.name).get(item.rm_item_code) \ - if po_transferred_qty_map.get(order.name).get(item.rm_item_code) else 0 - row ={ - 'purchase_order': item.parent, - 'date': order.transaction_date, - 'supplier': order.supplier, - 'rm_item_code': item.rm_item_code, - 'r_qty': item.required_qty, - 't_qty':transferred_qty, - 'p_qty':item.required_qty - transferred_qty - } + return data - data.append(row) - - return(data) - -def get_po(filters): - record_filters = [ - ["is_subcontracted", "=", "Yes"], - ["supplier", "=", filters.supplier], - ["transaction_date", "<=", filters.to_date], - ["transaction_date", ">=", filters.from_date], - ["docstatus", "=", 1] - ] - return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]) - -def get_transferred_quantity(po_name): - stock_entries = get_stock_entry(po_name) - stock_entries_detail = get_stock_entry_detail([v.name for v in stock_entries]) - po_transferred_qty_map = {} - - - for entry in stock_entries: - for details in stock_entries_detail: - if details.parent == entry.name: - details["Purchase_order"] = entry.purchase_order - if entry.purchase_order not in po_transferred_qty_map: - po_transferred_qty_map[entry.purchase_order] = {} - po_transferred_qty_map[entry.purchase_order][details.item_code] = details.qty - else: - po_transferred_qty_map[entry.purchase_order][details.item_code] = po_transferred_qty_map[entry.purchase_order].get(details.item_code, 0) + details.qty - - return po_transferred_qty_map - - -def get_stock_entry(po): - return frappe.get_all("Stock Entry", filters=[ - ('purchase_order', 'IN', po), - ('stock_entry_type', '=', 'Send to Subcontractor'), - ('docstatus', '=', 1) - ], fields=["name", "purchase_order"]) - -def get_stock_entry_detail(se): - return frappe.get_all("Stock Entry Detail", filters=[ - ["parent", "in", se] +def get_po_items_to_supply(filters): + return frappe.db.get_all( + "Purchase Order", + fields=[ + "name as purchase_order", + "transaction_date as date", + "supplier as supplier", + "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code", + "`tabPurchase Order Item Supplied`.required_qty as reqd_qty", + "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty" ], - fields=["parent", "item_code", "qty"]) - -def get_purchase_order_item_supplied(po): - return frappe.get_all("Purchase Order Item Supplied", filters=[ - ('parent', 'IN', po) - ], fields=['parent', 'rm_item_code', 'required_qty']) + filters = [ + ["Purchase Order", "per_received", "<", "100"], + ["Purchase Order", "is_subcontracted", "=", "Yes"], + ["Purchase Order", "supplier", "=", filters.supplier], + ["Purchase Order", "transaction_date", "<=", filters.to_date], + ["Purchase Order", "transaction_date", ">=", filters.from_date], + ["Purchase Order", "docstatus", "=", 1] + ] + ) \ No newline at end of file From ec4a3723cc1343e1ce99d9114ecb9bd610f8c034 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Jun 2021 12:47:06 +0530 Subject: [PATCH 077/550] fix: Sider --- .../subcontracted_raw_materials_to_be_transferred.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index 5a0381b017..68426abbb0 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -70,7 +70,7 @@ def get_data(filters): transferred_qty = row.get("transferred_qty") or 0 if transferred_qty < row.get("reqd_qty", 0): pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) - row.p_qty = pending_qty if pending_qty > 0 else 0 + row.p_qty = pending_qty if pending_qty > 0 else 0 data.append(row) return data From 5bb89b0ead96f0bb7e2346fb01bd8710bbb7a3b2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Jun 2021 15:57:01 +0530 Subject: [PATCH 078/550] test(perf): eliminate repeat creation of HSN codes (#25947) --- erpnext/regional/india/setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 229e0c031e..3e0b9b733b 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -27,6 +27,9 @@ def setup_company_independent_fixtures(patch=False): add_print_formats() def add_hsn_sac_codes(): + if frappe.flags.in_test and frappe.flags.created_hsn_codes: + return + # HSN codes with open(os.path.join(os.path.dirname(__file__), 'hsn_code_data.json'), 'r') as f: hsn_codes = json.loads(f.read()) @@ -38,6 +41,9 @@ def add_hsn_sac_codes(): sac_codes = json.loads(f.read()) create_hsn_codes(sac_codes, code_field="sac_code") + if frappe.flags.in_test: + frappe.flags.created_hsn_codes = True + def create_hsn_codes(data, code_field): for d in data: hsn_code = frappe.new_doc('GST HSN Code') From a9c84f749a64f2627885d61d571bf10c04fd61a8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Jun 2021 16:00:48 +0530 Subject: [PATCH 079/550] perf(minor): remove unnecessary comprehensions (#25645) --- erpnext/accounts/doctype/c_form/c_form.py | 2 +- .../doctype/coupon_code/coupon_code.py | 2 +- .../invoice_discounting/invoice_discounting.py | 2 +- .../doctype/journal_entry/journal_entry.py | 4 ++-- .../monthly_distribution.py | 2 +- .../doctype/payment_entry/payment_entry.py | 10 +++++----- .../doctype/payment_request/payment_request.py | 2 +- erpnext/accounts/doctype/pricing_rule/utils.py | 18 +++++++++--------- .../purchase_invoice/test_purchase_invoice.py | 2 +- .../test_tax_withholding_category.py | 2 +- erpnext/accounts/general_ledger.py | 2 +- .../report/cash_flow/custom_cash_flow.py | 8 ++++---- .../customer_ledger_summary.py | 2 +- .../accounts/report/financial_statements.py | 2 +- .../item_wise_purchase_register.py | 2 +- .../item_wise_sales_register.py | 2 +- .../report/pos_register/pos_register.py | 6 +++--- .../purchase_register/purchase_register.py | 14 +++++++------- .../sales_payment_summary.py | 4 ++-- .../report/sales_register/sales_register.py | 16 ++++++++-------- .../tds_computation_summary.py | 4 ++-- erpnext/accounts/report/utils.py | 2 +- erpnext/accounts/utils.py | 2 +- erpnext/assets/doctype/asset/test_asset.py | 2 +- .../asset_value_adjustment.py | 2 +- .../doctype/purchase_order/purchase_order.py | 6 +++--- .../purchase_order/test_purchase_order.py | 2 +- .../request_for_quotation.py | 2 +- erpnext/controllers/accounts_controller.py | 8 ++++---- erpnext/controllers/buying_controller.py | 6 +++--- erpnext/controllers/queries.py | 2 +- erpnext/controllers/selling_controller.py | 2 +- erpnext/controllers/status_updater.py | 4 ++-- erpnext/controllers/stock_controller.py | 6 +++--- erpnext/controllers/taxes_and_totals.py | 12 ++++++------ erpnext/controllers/tests/test_mapper.py | 4 ++-- .../controllers/website_list_for_contact.py | 2 +- .../manufacturing/doctype/job_card/job_card.py | 2 +- erpnext/projects/doctype/task/task.py | 2 +- .../projects/doctype/timesheet/timesheet.py | 4 ++-- .../report/project_summary/project_summary.py | 2 +- erpnext/selling/doctype/customer/customer.py | 2 +- erpnext/selling/doctype/quotation/quotation.py | 2 +- .../selling/doctype/sales_order/sales_order.py | 2 +- erpnext/setup/doctype/company/company.py | 4 ++-- erpnext/setup/doctype/item_group/item_group.py | 4 ++-- erpnext/stock/doctype/batch/batch.py | 2 +- .../doctype/delivery_note/delivery_note.py | 2 +- .../doctype/delivery_trip/delivery_trip.py | 6 +++--- .../landed_cost_voucher/landed_cost_voucher.py | 8 ++++---- .../test_landed_cost_voucher.py | 2 +- .../stock/doctype/packing_slip/packing_slip.py | 4 ++-- .../purchase_receipt/test_purchase_receipt.py | 2 +- erpnext/stock/doctype/serial_no/serial_no.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../item_price_stock/item_price_stock.py | 2 +- .../itemwise_recommended_reorder_level.py | 2 +- .../product_bundle_balance.py | 2 +- .../report/stock_balance/stock_balance.py | 6 +++--- .../stock/report/stock_ledger/stock_ledger.py | 4 ++-- erpnext/stock/stock_ledger.py | 2 +- .../doctype/warranty_claim/warranty_claim.py | 2 +- erpnext/utilities/product.py | 2 +- erpnext/utilities/transaction_base.py | 4 ++-- 64 files changed, 126 insertions(+), 126 deletions(-) diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py index fd86ed4c90..cfe28f3ff9 100644 --- a/erpnext/accounts/doctype/c_form/c_form.py +++ b/erpnext/accounts/doctype/c_form/c_form.py @@ -54,7 +54,7 @@ class CForm(Document): frappe.throw(_("Please enter atleast 1 invoice in the table")) def set_total_invoiced_amount(self): - total = sum([flt(d.grand_total) for d in self.get('invoices')]) + total = sum(flt(d.grand_total) for d in self.get('invoices')) frappe.db.set(self, 'total_invoiced_amount', total) @frappe.whitelist() diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.py b/erpnext/accounts/doctype/coupon_code/coupon_code.py index 7829c9320d..55c119315e 100644 --- a/erpnext/accounts/doctype/coupon_code/coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/coupon_code.py @@ -14,7 +14,7 @@ class CouponCode(Document): if not self.coupon_code: if self.coupon_type == "Promotional": - self.coupon_code =''.join([i for i in self.coupon_name if not i.isdigit()])[0:8].upper() + self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper() elif self.coupon_type == "Gift Card": self.coupon_code = frappe.generate_hash()[:10].upper() diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index 95d2ee56d9..b73d8bfbb1 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -42,7 +42,7 @@ class InvoiceDiscounting(AccountsController): record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice))) def calculate_total_amount(self): - self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices]) + self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices) def on_submit(self): self.update_sales_invoice() diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ed1bd28223..937597bc55 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -196,8 +196,8 @@ class JournalEntry(AccountsController): frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account)) def check_credit_limit(self): - customers = list(set([d.party for d in self.get("accounts") - if d.party_type=="Customer" and d.party and flt(d.debit) > 0])) + customers = list(set(d.party for d in self.get("accounts") + if d.party_type=="Customer" and d.party and flt(d.debit) > 0)) if customers: from erpnext.selling.doctype.customer.customer import check_credit_limit for customer in customers: diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py index 88667d7207..bff6422732 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py @@ -21,7 +21,7 @@ class MonthlyDistribution(Document): idx += 1 def validate(self): - total = sum([flt(d.percentage_allocation) for d in self.get("percentages")]) + total = sum(flt(d.percentage_allocation) for d in self.get("percentages")) if flt(total, 2) != 100.0: frappe.throw(_("Percentage Allocation should be equal to 100%") + \ diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index edca210142..2c6deb3896 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -309,7 +309,7 @@ class PaymentEntry(AccountsController): for k, v in no_oustanding_refs.items(): frappe.msgprint( _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") - .format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount")) + .format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount")) + "

" + _("If this is undesirable please cancel the corresponding Payment Entry."), title=_("Warning"), indicator="orange") @@ -524,7 +524,7 @@ class PaymentEntry(AccountsController): def set_unallocated_amount(self): self.unallocated_amount = 0 if self.party: - total_deductions = sum([flt(d.amount) for d in self.get("deductions")]) + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): @@ -549,7 +549,7 @@ class PaymentEntry(AccountsController): else: self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) - total_deductions = sum([flt(d.amount) for d in self.get("deductions")]) + total_deductions = sum(flt(d.amount) for d in self.get("deductions")) self.difference_amount = flt(self.difference_amount - total_deductions, self.precision("difference_amount")) @@ -565,8 +565,8 @@ class PaymentEntry(AccountsController): if ((self.payment_type=="Pay" and self.party_type=="Customer") or (self.payment_type=="Receive" and self.party_type=="Supplier")): - total_negative_outstanding = sum([abs(flt(d.outstanding_amount)) - for d in self.get("references") if flt(d.outstanding_amount) < 0]) + total_negative_outstanding = sum(abs(flt(d.outstanding_amount)) + for d in self.get("references") if flt(d.outstanding_amount) < 0) paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount additional_charges = sum([flt(d.amount) for d in self.deductions]) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 468978785b..438951db62 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -112,7 +112,7 @@ class PaymentRequest(Document): if not data_of_completed_requests: return self.grand_total - request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests]) + request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests) return request_amounts def on_cancel(self): diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index d23b952bdc..b54d0e73a8 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -20,9 +20,9 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money class MultiplePricingRuleConflict(frappe.ValidationError): pass apply_on_table = { - 'Item Code': 'items', - 'Item Group': 'item_groups', - 'Brand': 'brands' + 'Item Code': 'items', + 'Item Group': 'item_groups', + 'Brand': 'brands' } def get_pricing_rules(args, doc=None): @@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): condition = "ifnull({table}.{field}, '') in ({parent_groups})".format( table=table, field=field, - parent_groups=", ".join([frappe.db.escape(d) for d in parent_groups]) + parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups) ) frappe.flags.tree_conditions[key] = condition @@ -264,7 +264,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None): # find pricing rule with highest priority if pricing_rules: - max_priority = max([cint(p.priority) for p in pricing_rules]) + max_priority = max(cint(p.priority) for p in pricing_rules) if max_priority: pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules)) @@ -272,14 +272,14 @@ def filter_pricing_rules(args, pricing_rules, doc=None): pricing_rules = list(pricing_rules) if len(pricing_rules) > 1: - rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules])) + rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules)) if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage": pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \ or pricing_rules if len(pricing_rules) > 1 and not args.for_shopping_cart: frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}") - .format("\n".join([d.name for d in pricing_rules])), MultiplePricingRuleConflict) + .format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict) elif pricing_rules: return pricing_rules[0] @@ -541,7 +541,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): if pricing_rule_args: - items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item]) + items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item) for args in pricing_rule_args: if not items or (args.get('item_code'), args.get('pricing_rules')) not in items: @@ -589,4 +589,4 @@ def update_coupon_code_count(coupon_name,transaction_type): elif transaction_type=='cancelled': if coupon.used>0: coupon.used=coupon.used-1 - coupon.save(ignore_permissions=True) \ No newline at end of file + coupon.save(ignore_permissions=True) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 723d151ad8..503dda7728 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -632,7 +632,7 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(len(pi.get("supplied_items")), 2) - rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")]) + rm_supp_cost = sum(d.amount for d in pi.get("supplied_items")) self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2)) def test_rejected_serial_no(self): diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 0cea7612dd..dd26be7c99 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -112,7 +112,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): si = create_sales_invoice(customer = "Test TCS Customer", rate=5000) si.submit() - tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC') self.assertEqual(tcs_charged, 500) invoices.append(si) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index d4b249429b..59009ae621 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -143,7 +143,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False): validate_expense_against_budget(args) def validate_cwip_accounts(gl_map): - cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) + cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")) if cwip_enabled and gl_map[0].voucher_type == "Journal Entry": cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index ff87276a87..c11c15390b 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -32,7 +32,7 @@ def get_accounts_in_mappers(mapping_names): join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name where cfma.parent in (%s) order by cfm.is_working_capital - ''', (', '.join(['"%s"' % d for d in mapping_names]))) + ''', (', '.join('"%s"' % d for d in mapping_names))) def setup_mappers(mappers): @@ -83,8 +83,8 @@ def setup_mappers(mappers): account_types_labels = sorted( set( - [(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in account_types] + (d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense']) + for d in account_types ), key=lambda x: x[1] ) @@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) - accounts = ', '.join(['"%s"' % d for d in account_names]) + accounts = ', '.join('"%s"' % d for d in account_names) if opening_balances: date_info = dict(date=start_date) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 10b32fea56..c79d7401e6 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object): out = [] for party, row in iteritems(self.party_data): if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount: - total_party_adjustment = sum([amount for amount in itervalues(self.party_adjustment_details.get(party, {}))]) + total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {}))) row.paid_amount -= total_party_adjustment adjustments = self.party_adjustment_details.get(party, {}) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index d20ddbde5c..39ff804518 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -369,7 +369,7 @@ def set_gl_entries_by_account( if accounts: additional_conditions += " and account in ({})"\ - .format(", ".join([frappe.db.escape(d) for d in accounts])) + .format(", ".join(frappe.db.escape(d) for d in accounts)) gl_filters = { "company": company, diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index cb4d9b43db..685419a17e 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -334,7 +334,7 @@ def get_aii_accounts(): def get_purchase_receipts_against_purchase_order(item_list): po_pr_map = frappe._dict() - po_item_rows = list(set([d.po_detail for d in item_list])) + po_item_rows = list(set(d.po_detail for d in item_list)) if po_item_rows: purchase_receipts = frappe.db.sql(""" diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 928b373eff..2e794da842 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -23,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) - mode_of_payments = get_mode_of_payments(set([d.parent for d in item_list])) + mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list)) so_dn_map = get_delivery_notes_against_sales_order(item_list) data = [] diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py index cfbd7fd0c8..6a42bb4fb6 100644 --- a/erpnext/accounts/report/pos_register/pos_register.py +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field): ), filters, as_dict=1) def concat_mode_of_payments(pos_entries): - mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries])) + mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries)) for entry in pos_entries: if mode_of_payments.get(entry.pos_invoice): entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, [])) def add_subtotal_row(data, group_invoices, group_by_field, group_by_value): - grand_total = sum([d.grand_total for d in group_invoices]) - paid_amount = sum([d.paid_amount for d in group_invoices]) + grand_total = sum(d.grand_total for d in group_invoices) + paid_amount = sum(d.paid_amount for d in group_invoices) data.append({ group_by_field: group_by_value, "grand_total": grand_total, diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 8ac749d629..10edd41aa8 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -26,7 +26,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts) invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) - suppliers = list(set([d.supplier for d in invoice_list])) + suppliers = list(set(d.supplier for d in invoice_list)) supplier_details = get_supplier_details(suppliers) company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") @@ -120,13 +120,13 @@ def get_columns(invoice_list, additional_table_columns): and docstatus = 1 and (account_head is not null and account_head != '') and category in ('Total', 'Valuation and Total') and parent in (%s) order by account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account from `tabPurchase Invoice` where docstatus = 1 and name in (%s) and ifnull(unrealized_profit_loss_account, '') != '' order by unrealized_profit_loss_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts] unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts] @@ -208,7 +208,7 @@ def get_invoice_expense_map(invoice_list): from `tabPurchase Invoice Item` where parent in (%s) group by parent, expense_account - """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_expense_map = {} for d in expense_details: @@ -221,7 +221,7 @@ def get_internal_invoice_map(invoice_list): unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, base_net_total as amount from `tabPurchase Invoice` where name in (%s) and is_internal_supplier = 1 and company = represents_company""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) internal_invoice_map = {} for d in unrealized_amount_details: @@ -238,7 +238,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts): where parent in (%s) and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0 group by parent, account_head, add_deduct_tax - """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_tax_map = {} for d in tax_details: @@ -258,7 +258,7 @@ def get_invoice_po_pr_map(invoice_list): select parent, purchase_order, purchase_receipt, po_detail, project from `tabPurchase Invoice Item` where parent in (%s) - """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_po_pr_map = {} for d in pi_items: diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py index c234da0fe3..ff774681a2 100644 --- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py +++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py @@ -158,7 +158,7 @@ def get_sales_invoice_data(filters): def get_mode_of_payments(filters): mode_of_payments = {} invoice_list = get_invoices(filters) - invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list]) + invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) if invoice_list: inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment from `tabSales Invoice` a, `tabSales Invoice Payment` b @@ -197,7 +197,7 @@ def get_invoices(filters): def get_mode_of_payment_details(filters): mode_of_payment_details = {} invoice_list = get_invoices(filters) - invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list]) + invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list) if invoice_list: inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index cb2c98b64a..909959323f 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -248,19 +248,19 @@ def get_columns(invoice_list, additional_table_columns): income_accounts = frappe.db.sql_list("""select distinct income_account from `tabSales Invoice Item` where docstatus = 1 and parent in (%s) order by income_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) tax_accounts = frappe.db.sql_list("""select distinct account_head from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice' and docstatus = 1 and base_tax_amount_after_discount_amount != 0 and parent in (%s) order by account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account from `tabSales Invoice` where docstatus = 1 and name in (%s) and ifnull(unrealized_profit_loss_account, '') != '' order by unrealized_profit_loss_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list)) for account in income_accounts: income_columns.append({ @@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns): def get_invoice_income_map(invoice_list): income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_income_map = {} for d in income_details: @@ -419,7 +419,7 @@ def get_internal_invoice_map(invoice_list): unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account, base_net_total as amount from `tabSales Invoice` where name in (%s) and is_internal_customer = 1 and company = represents_company""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) internal_invoice_map = {} for d in unrealized_amount_details: @@ -432,7 +432,7 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts): tax_details = frappe.db.sql("""select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_tax_map = {} for d in tax_details: @@ -451,7 +451,7 @@ def get_invoice_so_dn_map(invoice_list): si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail from `tabSales Invoice Item` where parent in (%s) and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_so_dn_map = {} for d in si_items: @@ -475,7 +475,7 @@ def get_invoice_cc_wh_map(invoice_list): si_items = frappe.db.sql("""select parent, cost_center, warehouse from `tabSales Invoice Item` where parent in (%s) and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" % - ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1) + ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1) invoice_cc_wh_map = {} for d in si_items: diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index a8280c1b18..e15715dccd 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -78,7 +78,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f and company=%s and posting_date between %s and %s """, (supplier, company, from_date, to_date), as_dict=1) - supplier_credit_amount = flt(sum([d.credit for d in entries])) + supplier_credit_amount = flt(sum(d.credit for d in entries)) vouchers = [d.voucher_no for d in entries] vouchers += get_advance_vouchers([supplier], company=company, @@ -91,7 +91,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f from `tabGL Entry` where account=%s and posting_date between %s and %s and company=%s and credit > 0 and voucher_no in ({0}) - """.format(', '.join(["'%s'" % d for d in vouchers])), + """.format(', '.join("'%s'" % d for d in vouchers)), (account, from_date, to_date, company))[0][0]) date_range_filter = [fiscal_year, from_date, to_date] diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index b020d0a506..ba461edaf8 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -139,6 +139,6 @@ def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=N gross_profit_data = GrossProfitGenerator(filters) result = gross_profit_data.grouped_data if not with_item_data: - result = sum([d.gross_profit for d in result]) + result = sum(d.gross_profit for d in result) return result diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5a64e27ccb..66a9b60125 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -635,7 +635,7 @@ def get_held_invoices(party_type, party): 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()', as_dict=1 ) - held_invoices = set([d['name'] for d in held_invoices]) + held_invoices = set(d['name'] for d in held_invoices) return held_invoices diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 3cd4b802c1..8845f24d10 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase): }) asset.insert() accumulated_depreciation_after_full_schedule = \ - max([d.accumulated_depreciation_amount for d in asset.get("schedules")]) + max(d.accumulated_depreciation_amount for d in asset.get("schedules")) asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule)) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 14308277c1..2f6b5ee2dc 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -92,7 +92,7 @@ class AssetValueAdjustment(Document): d.value_after_depreciation = asset_value if d.depreciation_method in ("Straight Line", "Manual"): - end_date = max([s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx]) + end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx) total_days = date_diff(end_date, self.date) rate_per_day = flt(d.value_after_depreciation) / flt(total_days) from_date = self.date diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 782593a5c5..2629ba7d61 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -139,7 +139,7 @@ class PurchaseOrder(BuyingController): def validate_minimum_order_qty(self): if not self.get("items"): return - items = list(set([d.item_code for d in self.get("items")])) + items = list(set(d.item_code for d in self.get("items"))) itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items)) @@ -326,10 +326,10 @@ class PurchaseOrder(BuyingController): so.notify_update() def has_drop_ship_item(self): - return any([d.delivered_by_supplier for d in self.items]) + return any(d.delivered_by_supplier for d in self.items) def is_against_so(self): - return any([d.sales_order for d in self.items if d.sales_order]) + return any(d.sales_order for d in self.items if d.sales_order) def set_received_qty_for_drop_ship_items(self): for item in self.items: diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 39171960d8..3b9f8e9775 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -359,7 +359,7 @@ class TestPurchaseOrder(unittest.TestCase): update_child_qty_rate('Purchase Order', trans_item, po.name) po.reload() - total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) + total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items")) self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 180ba93666..0127eb8163 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -391,7 +391,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc = def get_supplier_tag(): if not frappe.cache().hget("Supplier", "Tags"): filters = {"document_type": "Supplier"} - tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag])) + tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag)) frappe.cache().hset("Supplier", "Tags", tags) return frappe.cache().hget("Supplier", "Tags") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 53ded33b6f..7c6061defa 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -610,8 +610,8 @@ class AccountsController(TransactionBase): order_field = "purchase_order" order_doctype = "Purchase Order" - order_list = list(set([d.get(order_field) - for d in self.get("items") if d.get(order_field)])) + order_list = list(set(d.get(order_field) + for d in self.get("items") if d.get(order_field))) journal_entries = get_advance_journal_entries(party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated) @@ -635,8 +635,8 @@ class AccountsController(TransactionBase): def validate_advance_entries(self): order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order" - order_list = list(set([d.get(order_field) - for d in self.get("items") if d.get(order_field)])) + order_list = list(set(d.get(order_field) + for d in self.get("items") if d.get(order_field))) if not order_list: return diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3f2d3390c0..da819119b1 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -181,8 +181,8 @@ class BuyingController(StockController): stock_and_asset_items_amount += flt(d.base_net_amount) last_item_idx = d.idx - total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") - if d.category in ["Valuation", "Valuation and Total"]]) + total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") + if d.category in ["Valuation", "Valuation and Total"]) valuation_amount_adjustment = total_valuation_amount for i, item in enumerate(self.get("items")): @@ -325,7 +325,7 @@ class BuyingController(StockController): def update_raw_materials_supplied_based_on_stock_entries(self): self.set('supplied_items', []) - purchase_orders = set([d.purchase_order for d in self.items]) + purchase_orders = set(d.purchase_order for d in self.items) # qty of raw materials backflushed (for each item per purchase order) backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 81ac234e70..7bd739a6ad 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -88,7 +88,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Customer", fields) searchfields = frappe.get_meta("Customer").get_search_fields() - searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) + searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) return frappe.db.sql("""select {fields} from `tabCustomer` where docstatus < 2 diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 54156f379c..7f28289760 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -428,7 +428,7 @@ class SellingController(StockController): self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): - doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) + doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname))) if doc_list: po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')] diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 83d4c33140..943f7aaeb1 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -299,8 +299,8 @@ class StatusUpdater(Document): args['name'] = self.get(args['percent_join_field_parent']) self._update_percent_field(args, update_modified) else: - distinct_transactions = set([d.get(args['percent_join_field']) - for d in self.get_all_children(args['source_dt'])]) + distinct_transactions = set(d.get(args['percent_join_field']) + for d in self.get_all_children(args['source_dt'])) for name in distinct_transactions: if name: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 0da723d56e..9c29b0076b 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -313,7 +313,7 @@ class StockController(AccountsController): def get_serialized_items(self): serialized_items = [] - item_codes = list(set([d.item_code for d in self.get("items")])) + item_codes = list(set(d.item_code for d in self.get("items"))) if item_codes: serialized_items = frappe.db.sql_list("""select name from `tabItem` where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))), @@ -324,8 +324,8 @@ class StockController(AccountsController): def validate_warehouse(self): from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company - warehouses = list(set([d.warehouse for d in - self.get("items") if getattr(d, "warehouse", None)])) + warehouses = list(set(d.warehouse for d in + self.get("items") if getattr(d, "warehouse", None))) target_warehouses = list(set([d.target_warehouse for d in self.get("items") if getattr(d, "target_warehouse", None)])) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 0b4fb3a3ed..2bb83ea7f0 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -378,10 +378,10 @@ class calculate_taxes_and_totals(object): def manipulate_grand_total_for_inclusive_tax(self): # if fully inclusive taxes and diff - if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]): + if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")): last_tax = self.doc.get("taxes")[-1] - non_inclusive_tax_amount = sum([flt(d.tax_amount_after_discount_amount) - for d in self.doc.get("taxes") if not d.included_in_print_rate]) + non_inclusive_tax_amount = sum(flt(d.tax_amount_after_discount_amount) + for d in self.doc.get("taxes") if not d.included_in_print_rate) diff = self.doc.total + non_inclusive_tax_amount \ - flt(last_tax.total, last_tax.precision("total")) @@ -521,8 +521,8 @@ class calculate_taxes_and_totals(object): def calculate_total_advance(self): if self.doc.docstatus < 2: - total_allocated_amount = sum([flt(adv.allocated_amount, adv.precision("allocated_amount")) - for adv in self.doc.get("advances")]) + total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount")) + for adv in self.doc.get("advances")) self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance")) @@ -622,7 +622,7 @@ class calculate_taxes_and_totals(object): if self.doc.doctype == "Sales Invoice" \ and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \ - and any([d.type == "Cash" for d in self.doc.payments]): + and any(d.type == "Cash" for d in self.doc.payments): grand_total = self.doc.rounded_total or self.doc.grand_total base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total diff --git a/erpnext/controllers/tests/test_mapper.py b/erpnext/controllers/tests/test_mapper.py index 66459fdbf8..7a4b2d3614 100644 --- a/erpnext/controllers/tests/test_mapper.py +++ b/erpnext/controllers/tests/test_mapper.py @@ -26,8 +26,8 @@ class TestMapper(unittest.TestCase): # Assert that all inserted items are present in updated sales order src_items = item_list_1 + item_list_2 + item_list_3 - self.assertEqual(set([d for d in src_items]), - set([d.item_code for d in updated_so.items])) + self.assertEqual(set(d for d in src_items), + set(d.item_code for d in updated_so.items)) def make_quotation(self, item_list, customer): diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index ecf041efd1..7c072e4fad 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -113,7 +113,7 @@ def post_process(doctype, data): doc.set_indicator() doc.status_display = ", ".join(doc.status_display) - doc.items_preview = ", ".join([d.item_name for d in doc.items if d.item_name]) + doc.items_preview = ", ".join(d.item_name for d in doc.items if d.item_name) result.append(doc) return result diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d764db33f8..cdc4518894 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -317,7 +317,7 @@ class JobCard(Document): 'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id') if job_cards: - qty = min([d.qty for d in job_cards]) + qty = min(d.qty for d in job_cards) doc.db_set('material_transferred_for_manufacturing', qty) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index d1583f1473..39a6024e2c 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -232,7 +232,7 @@ def get_project(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta(doctype) searchfields = meta.get_search_fields() search_columns = ", " + ", ".join(searchfields) if searchfields else '' - search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields]) + search_cond = " or " + " or ".join(field + " like %(txt)s" for field in searchfields) return frappe.db.sql(""" select name {search_columns} from `tabProject` where %(key)s like %(txt)s diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index a3e4577f90..c8bd80fca0 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -87,8 +87,8 @@ class Timesheet(Document): def set_dates(self): if self.docstatus < 2 and self.time_logs: - start_date = min([getdate(d.from_time) for d in self.time_logs]) - end_date = max([getdate(d.to_time) for d in self.time_logs]) + start_date = min(getdate(d.from_time) for d in self.time_logs) + end_date = max(getdate(d.to_time) for d in self.time_logs) if start_date and end_date: self.start_date = getdate(start_date) diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py index 2c7bb49cfb..98dd617f9b 100644 --- a/erpnext/projects/report/project_summary/project_summary.py +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -122,7 +122,7 @@ def get_report_summary(data): if not data: return None - avg_completion = sum([project.percent_complete for project in data]) / len(data) + avg_completion = sum(project.percent_complete for project in data) / len(data) total = sum([project.total_tasks for project in data]) total_overdue = sum([project.overdue_tasks for project in data]) completed = sum([project.completed_tasks for project in data]) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 51d86ff0bf..818888c0c1 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -75,7 +75,7 @@ class Customer(TransactionBase): self.loyalty_program_tier = customer.loyalty_program_tier if self.sales_team: - if sum([member.allocated_percentage or 0 for member in self.sales_team]) != 100: + if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) def check_customer_group_change(self): diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 246f9234a4..e4f8a47581 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -50,7 +50,7 @@ class Quotation(SellingController): self.customer_name = company_name or lead_name def update_opportunity(self, status): - for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])): + for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: self.update_opportunity_status(status, opportunity) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index d9e52e1d69..551f715bd5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -151,7 +151,7 @@ class SalesOrder(SellingController): frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0])) def update_prevdoc_status(self, flag=None): - for quotation in list(set([d.prevdoc_docname for d in self.get("items")])): + for quotation in set(d.prevdoc_docname for d in self.get("items")): if quotation: doc = frappe.get_doc("Quotation", quotation) if doc.docstatus==2: diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 077538d479..27e023c1e5 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -54,7 +54,7 @@ class Company(NestedSet): def validate_abbr(self): if not self.abbr: - self.abbr = ''.join([c[0] for c in self.company_name.split()]).upper() + self.abbr = ''.join(c[0] for c in self.company_name.split()).upper() self.abbr = self.abbr.strip() @@ -335,7 +335,7 @@ class Company(NestedSet): clear_defaults_cache() def abbreviate(self): - self.abbr = ''.join([c[0].upper() for c in self.company_name.split()]) + self.abbr = ''.join(c[0].upper() for c in self.company_name.split()) def on_trash(self): """ diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index bff806d547..db31d6d699 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -139,7 +139,7 @@ def get_product_list_for_group(product_group=None, start=0, limit=10, search=Non # return child item groups if the type is of "Is Group" return get_child_groups_for_list_in_html(item_group, start, limit, search) - child_groups = ", ".join([frappe.db.escape(i[0]) for i in get_child_groups(product_group)]) + child_groups = ", ".join(frappe.db.escape(i[0]) for i in get_child_groups(product_group)) # base query query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group, @@ -239,7 +239,7 @@ def get_item_for_list_in_html(context): return frappe.get_template(products_template).render(context) def get_group_item_count(item_group): - child_groups = ", ".join(['"' + i[0] + '"' for i in get_child_groups(item_group)]) + child_groups = ", ".join('"' + i[0] + '"' for i in get_child_groups(item_group)) return frappe.db.sql("""select count(*) from `tabItem` where docstatus = 0 and show_in_website = 1 and (item_group in (%s) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 8fdda565d2..508e17c340 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -304,7 +304,7 @@ def validate_serial_no_with_batch(serial_nos, item_code): frappe.throw(_("The serial no {0} does not belong to item {1}") .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code))) - serial_no_link = ','.join([get_link_to_form("Serial No", sn) for sn in serial_nos]) + serial_no_link = ','.join(get_link_to_form("Serial No", sn) for sn in serial_nos) message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" frappe.throw(_("There is no batch found against the {0}: {1}") diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index cce51cb9b1..dd31965fac 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -264,7 +264,7 @@ class DeliveryNote(SellingController): """ Validate that if packed qty exists, it should be equal to qty """ - if not any([flt(d.get('packed_qty')) for d in self.get("items")]): + if not any(flt(d.get('packed_qty')) for d in self.get("items")): return has_error = False for d in self.get("items"): diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 81e730126e..9ec28d8981 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -68,7 +68,7 @@ class DeliveryTrip(Document): delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. """ - delivery_notes = list(set([stop.delivery_note for stop in self.delivery_stops if stop.delivery_note])) + delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)) update_fields = { "driver": self.driver, @@ -136,8 +136,8 @@ class DeliveryTrip(Document): # Include last leg in the final distance calculation self.uom = self.default_distance_uom - total_distance = sum([leg.get("distance", {}).get("value", 0.0) - for leg in directions.get("legs")]) # in meters + total_distance = sum(leg.get("distance", {}).get("value", 0.0) + for leg in directions.get("legs")) # in meters self.total_distance = total_distance * self.uom_conversion_factor else: idx += len(route) - 1 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 83109469fc..5df4d8743f 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -78,7 +78,7 @@ class LandedCostVoucher(Document): .format(item.idx, item.item_code)) def set_total_taxes_and_charges(self): - self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")]) + self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes")) def set_applicable_charges_on_item(self): if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': @@ -104,15 +104,15 @@ class LandedCostVoucher(Document): based_on = self.distribute_charges_based_on.lower() if based_on != 'distribute manually': - total = sum([flt(d.get(based_on)) for d in self.get("items")]) + total = sum(flt(d.get(based_on)) for d in self.get("items")) else: # consider for proportion while distributing manually - total = sum([flt(d.get('applicable_charges')) for d in self.get("items")]) + total = sum(flt(d.get('applicable_charges')) for d in self.get("items")) if not total: frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) - total_applicable_charges = sum([flt(d.applicable_charges) for d in self.get("items")]) + total_applicable_charges = sum(flt(d.applicable_charges) for d in self.get("items")) precision = get_field_precision(frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), currency=frappe.get_cached_value('Company', self.company, "default_currency")) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 984ae46c66..32b08f60c4 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -311,7 +311,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, def distribute_landed_cost_on_items(lcv): based_on = lcv.distribute_charges_based_on.lower() - total = sum([flt(d.get(based_on)) for d in lcv.get("items")]) + total = sum(flt(d.get(based_on)) for d in lcv.get("items")) for item in lcv.get("items"): item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total) diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 2008bffcd3..4a843e0fde 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -88,9 +88,9 @@ class PackingSlip(Document): rows = [d.item_code for d in self.get("items")] # also pick custom fields from delivery note - custom_fields = ', '.join(['dni.`{0}`'.format(d.fieldname) + custom_fields = ', '.join('dni.`{0}`'.format(d.fieldname) for d in frappe.get_meta("Delivery Note Item").get_custom_fields() - if d.fieldtype not in no_value_fields]) + if d.fieldtype not in no_value_fields) if custom_fields: custom_fields = ', ' + custom_fields diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 5095a80214..8d9b675bed 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -246,7 +246,7 @@ class TestPurchaseReceipt(unittest.TestCase): pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes") self.assertEqual(len(pr.get("supplied_items")), 2) - rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) + rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) pr.cancel() diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 5ecc9f8140..b236f6a999 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -613,7 +613,7 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]): batch_nos = filters.get("batch_no") expiry_date = filters.get("expiry_date") if batch_nos: - batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join(["'%s'" % d for d in batch_nos])) + batch_no_condition = """and sr.batch_no in ({}) """.format(', '.join("'%s'" % d for d in batch_nos)) if expiry_date: batch_join_selection = "LEFT JOIN `tabBatch` batch on sr.batch_no = batch.name " diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2f76bc7d56..560ceaa917 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -465,7 +465,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item]) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) # Set basic rate for incoming items for d in self.get('items'): diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 5296211fae..db7498bb21 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -89,7 +89,7 @@ def get_item_price_qty_data(filters): {conditions}""" .format(conditions=conditions), filters, as_dict=1) - price_list_names = list(set([item.price_list_name for item in item_results])) + price_list_names = list(set(item.price_list_name for item in item_results)) buying_price_map = get_price_map(price_list_names, buying=1) selling_price_map = get_price_map(price_list_names, selling=1) diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index 2f70523264..2e13aa0b04 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -66,7 +66,7 @@ def get_consumed_items(condition): purpose is NULL or purpose not in ({}) ) - """.format(', '.join([f"'{p}'" for p in purpose_to_exclude])) + """.format(', '.join(f"'{p}'" for p in purpose_to_exclude)) condition = condition.replace("posting_date", "sle.posting_date") consumed_items = frappe.db.sql(""" diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 276e42ee43..8fffbccab3 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -141,7 +141,7 @@ def get_stock_ledger_entries(filters, items): return [] item_conditions_sql = ' and sle.item_code in ({})' \ - .format(', '.join([frappe.db.escape(i) for i in items])) + .format(', '.join(frappe.db.escape(i) for i in items)) conditions = get_sle_conditions(filters) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index bbd73e9112..b6a8063189 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -157,7 +157,7 @@ def get_stock_ledger_entries(filters, items): item_conditions_sql = '' if items: item_conditions_sql = ' and sle.item_code in ({})'\ - .format(', '.join([frappe.db.escape(i, percent=False) for i in items])) + .format(', '.join(frappe.db.escape(i, percent=False) for i in items)) conditions = get_conditions(filters) @@ -253,7 +253,7 @@ def get_items(filters): def get_item_details(items, sle, filters): item_details = {} if not items: - items = list(set([d.item_code for d in sle])) + items = list(set(d.item_code for d in sle)) if not items: return item_details @@ -291,7 +291,7 @@ def get_item_reorder_details(items): select parent, warehouse, warehouse_reorder_qty, warehouse_reorder_level from `tabItem Reorder` where parent in ({0}) - """.format(', '.join([frappe.db.escape(i, percent=False) for i in items])), as_dict=1) + """.format(', '.join(frappe.db.escape(i, percent=False) for i in items)), as_dict=1) return dict((d.parent + d.warehouse, d) for d in item_reorder_details) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 36996e9674..8909f217f4 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -115,7 +115,7 @@ def get_stock_ledger_entries(filters, items): item_conditions_sql = '' if items: item_conditions_sql = 'and sle.item_code in ({})'\ - .format(', '.join([frappe.db.escape(i) for i in items])) + .format(', '.join(frappe.db.escape(i) for i in items)) sl_entries = frappe.db.sql(""" SELECT @@ -169,7 +169,7 @@ def get_items(filters): def get_item_details(items, sl_entries, include_uom): item_details = {} if not items: - items = list(set([d.item_code for d in sl_entries])) + items = list(set(d.item_code for d in sl_entries)) if not items: return item_details diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b2825fc26f..fc82c789cc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -521,7 +521,7 @@ class update_entries_after(object): fields=["purchase_rate", "name", "company"], filters = {'name': ('in', serial_nos)}) - incoming_values = sum([flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company]) + incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company) # Get rate for serial nos which has been transferred to other company invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company] diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py index a3428a25af..a20e7a801b 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py @@ -29,7 +29,7 @@ class WarrantyClaim(TransactionBase): where t2.parent = t1.name and t2.prevdoc_docname = %s and t1.docstatus!=2""", (self.name)) if lst: - lst1 = ','.join([x[0] for x in lst]) + lst1 = ','.join(x[0] for x in lst) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) else: frappe.db.set(self, 'status', 'Cancelled') diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 66d6cd3888..70b41767d6 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -131,6 +131,6 @@ def get_non_stock_item_status(item_code, item_warehouse_field): if frappe.db.exists("Product Bundle", item_code): items = frappe.get_doc("Product Bundle", item_code).get_all_children() bundle_warehouse = frappe.db.get_value('Item', item_code, item_warehouse_field) - return all([ get_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items ]) + return all(get_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items) else: return 1 diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index f99da58e46..db997263c1 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -147,7 +147,7 @@ class TransactionBase(StatusUpdater): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): fieldname = self.prev_link_mapper[for_doctype]["fieldname"] - values = filter(None, tuple([item.as_dict()[fieldname] for item in self.items])) + values = filter(None, tuple(item.as_dict()[fieldname] for item in self.items)) if values: ret = { @@ -180,7 +180,7 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, string_types): qty_fields = [qty_fields] - distinct_uoms = list(set([d.get(uom_field) for d in doc.get_all_children()])) + distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) integer_uoms = list(filter(lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, distinct_uoms)) From c4d851e45f591667a33e3fb7e50c5bf1cf14a993 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Jun 2021 17:27:43 +0530 Subject: [PATCH 080/550] fix: Test --- ...tracted_raw_materials_to_be_transferred.py | 68 ++++++++++++++----- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index c1fc6fb82f..11ec7669b0 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -12,34 +12,68 @@ import json, frappe, unittest class TestSubcontractedItemToBeTransferred(unittest.TestCase): def test_pending_and_transferred_qty(self): - po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') + po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC") + + # Material Receipt of RMs make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100) make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100) - transfer_subcontracted_raw_materials(po.name) - col, data = execute(filters=frappe._dict({'supplier': po.supplier, - 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)), - 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))})) - self.assertEqual(data[0]['purchase_order'], po.name) - self.assertIn(data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) - self.assertIn(data[0]['p_qty'], [9, 18]) - self.assertIn(data[0]['t_qty'], [1, 2]) + + se = transfer_subcontracted_raw_materials(po) + + col, data = execute(filters=frappe._dict( + { + 'supplier': po.supplier, + 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)), + 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)) + } + )) + po.reload() + + po_data = [row for row in data if row.get('purchase_order') == po.name] + + self.assertEqual(len(po_data), 2) + self.assertIn(po_data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) + self.assertIn(po_data[0]['p_qty'], [9, 18]) + self.assertIn(po_data[0]['transferred_qty'], [1, 2]) self.assertEqual(data[1]['purchase_order'], po.name) self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) self.assertIn(data[1]['p_qty'], [9, 18]) - self.assertIn(data[1]['t_qty'], [1, 2]) + self.assertIn(data[1]['transferred_qty'], [1, 2]) + se.cancel() + po.cancel() def transfer_subcontracted_raw_materials(po): rm_item = [ - {'item_code': '_Test Item', 'rm_item_code': '_Test Item', 'item_name': '_Test Item', 'qty': 1, - 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100, 'stock_uom': 'Nos'}, - {'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2, - 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}] + { + 'name': po.supplied_items[0].name, + 'item_code': '_Test Item Home Desktop 100', + 'rm_item_code': '_Test Item Home Desktop 100', + 'item_name': '_Test Item Home Desktop 100', + 'qty': 2, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 200, + 'stock_uom': 'Nos' + }, + { + 'name': po.supplied_items[1].name, + 'item_code': '_Test Item', + 'rm_item_code': '_Test Item', + 'item_name': '_Test Item', + 'qty': 1, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 100, + 'stock_uom': 'Nos' + } + ] rm_item_string = json.dumps(rm_item) - se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string)) - se.from_warehouse = '_Test Warehouse 1 - _TC' - se.to_warehouse = '_Test Warehouse 1 - _TC' + se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + se.from_warehouse = '_Test Warehouse - _TC' + se.to_warehouse = '_Test Warehouse - _TC' se.stock_entry_type = 'Send to Subcontractor' se.save() se.submit() + return se From 400205cc8a9632614617af2f21d598491db8cde8 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Sat, 12 Jun 2021 12:30:53 +0530 Subject: [PATCH 081/550] fix: payroll entry employee detail issue (#25968) * fix: payroll entry employee detail issue * fix: review changes --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 3953b463f1..7a70679db4 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -665,6 +665,8 @@ def get_employee_list(filters): emp_list = remove_payrolled_employees(emp_list, filters.start_date, filters.end_date) return emp_list + return [] + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): From 17550fb4bfb968ea5c0be9ab1e233ac55ed35936 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 12 Jun 2021 13:33:21 +0530 Subject: [PATCH 082/550] feat: add Inactive status to Employee --- erpnext/accounts/party.py | 2 +- erpnext/hr/doctype/employee/employee.json | 4 ++-- erpnext/hr/doctype/employee/employee.py | 4 ++-- erpnext/hr/doctype/employee/employee_list.js | 2 +- erpnext/hr/doctype/employee_promotion/employee_promotion.py | 6 +++--- erpnext/hr/doctype/employee_transfer/employee_transfer.py | 6 +++--- erpnext/payroll/doctype/retention_bonus/retention_bonus.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e01cb6e151..e025fc6905 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -457,7 +457,7 @@ def validate_party_frozen_disabled(party_type, party_name): frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen) elif party_type == "Employee": - if frappe.db.get_value("Employee", party_name, "status") == "Left": + if frappe.db.get_value("Employee", party_name, "status") != "Active": frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True) def get_timeline_data(doctype, name): diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 5123d6a5a7..5442ed56c3 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -207,7 +207,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Active\nLeft", + "options": "Active\nInactive\nLeft", "reqd": 1, "search_index": 1 }, @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2021-01-02 16:54:33.477439", + "modified": "2021-06-12 11:31:37.730760", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index ed7d588434..bc5694226a 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -37,7 +37,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Active", "Temporary Leave", "Left"]) + validate_status(self.status, ["Active", "Inactive", "Left"]) self.employee = self.name self.set_employee_name() @@ -478,7 +478,7 @@ def get_employee_emails(employee_list): @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [['status', '!=', 'Left']] + filters = [['status', '=', 'Active']] if company and company != 'All Companies': filters.append(['company', '=', company]) diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js index 44837030be..6679e318c2 100644 --- a/erpnext/hr/doctype/employee/employee_list.js +++ b/erpnext/hr/doctype/employee/employee_list.js @@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = { filters: [["status","=", "Active"]], get_indicator: function(doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Temporary Leave": "red", "Left": "gray"}[doc.status]; + indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; return indicator; } }; diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py index 4994921268..83fb235f92 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee class EmployeePromotion(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") == "Left": - frappe.throw(_("Cannot promote Employee with status Left")) + if frappe.get_value("Employee", self.employee, "status") != "Active": + frappe.throw(_("Cannot promote Employee with status Left or Inactive")) def before_submit(self): if getdate(self.promotion_date) > getdate(): - frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date "), + frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date"), frappe.DocstatusTransitionError) def on_submit(self): diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py index 3539970a32..6eec9fa12a 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee class EmployeeTransfer(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") == "Left": - frappe.throw(_("Cannot transfer Employee with status Left")) + if frappe.get_value("Employee", self.employee, "status") != "Active": + frappe.throw(_("Cannot transfer Employee with status Left or Inactive")) def before_submit(self): if getdate(self.transfer_date) > getdate(): - frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date "), + frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), frappe.DocstatusTransitionError) def on_submit(self): diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py index b8e56ae42a..049ea265cc 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -10,8 +10,8 @@ from frappe.utils import getdate class RetentionBonus(Document): def validate(self): - if frappe.get_value('Employee', self.employee, 'status') == 'Left': - frappe.throw(_('Cannot create Retention Bonus for left Employees')) + if frappe.get_value('Employee', self.employee, 'status') != 'Active': + frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees')) if getdate(self.bonus_payment_date) < getdate(): frappe.throw(_('Bonus Payment Date cannot be a past date')) From cf349aadf9ea7108bb0a95b86da23ffab87d62e5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 12 Jun 2021 14:05:12 +0530 Subject: [PATCH 083/550] fix: unable to enter score in Assessment Result details grid (#25945) (#26031) --- .../education/doctype/assessment_result/assessment_result.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/education/doctype/assessment_result/assessment_result.js b/erpnext/education/doctype/assessment_result/assessment_result.js index 617a873b82..c35f607a99 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result.js +++ b/erpnext/education/doctype/assessment_result/assessment_result.js @@ -6,7 +6,8 @@ frappe.ui.form.on('Assessment Result', { if (!frm.doc.__islocal) { frm.trigger('setup_chart'); } - frm.set_df_property('details', 'read_only', 1); + + frm.get_field('details').grid.cannot_add_rows = true; frm.set_query('course', function() { return { From 28cdff10cfe981c6e91b1a9ef897638e7c800af6 Mon Sep 17 00:00:00 2001 From: Anoop <3326959+akurungadam@users.noreply.github.com> Date: Sat, 12 Jun 2021 14:17:04 +0530 Subject: [PATCH 084/550] fix: update linked Customer on Patient update only if Link Customer to Patient is enabled (#25926) --- erpnext/healthcare/doctype/patient/patient.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 789d452c07..cebcb2068e 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -33,21 +33,21 @@ class Patient(Document): self.reload() # self.notify_update() def on_update(self): - if self.customer: - customer = frappe.get_doc('Customer', self.customer) - if self.customer_group: - customer.customer_group = self.customer_group - if self.territory: - customer.territory = self.territory + if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'): + if self.customer: + customer = frappe.get_doc('Customer', self.customer) + if self.customer_group: + customer.customer_group = self.customer_group + if self.territory: + customer.territory = self.territory - customer.customer_name = self.patient_name - customer.default_price_list = self.default_price_list - customer.default_currency = self.default_currency - customer.language = self.language - customer.ignore_mandatory = True - customer.save(ignore_permissions=True) - else: - if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'): + customer.customer_name = self.patient_name + customer.default_price_list = self.default_price_list + customer.default_currency = self.default_currency + customer.language = self.language + customer.ignore_mandatory = True + customer.save(ignore_permissions=True) + else: create_customer(self) def set_full_name(self): From 580346360fc3a6dd168e726cd035f5cc42f8f3c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 11:16:39 +0530 Subject: [PATCH 085/550] fix: Auto tax calculations in Payment Entry --- .../doctype/payment_entry/payment_entry.js | 86 +++++++++++++++++-- .../doctype/payment_entry/payment_entry.py | 44 +++++----- .../purchase_taxes_and_charges.json | 3 +- .../sales_taxes_and_charges.json | 3 +- erpnext/controllers/accounts_controller.py | 34 ++++---- 5 files changed, 118 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 939f3546ff..d3ac3a6676 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1087,6 +1087,8 @@ frappe.ui.form.on('Payment Entry', { initialize_taxes: function(frm) { $.each(frm.doc["taxes"] || [], function(i, tax) { + frm.events.validate_taxes_and_charges(tax); + frm.events.validate_inclusive_tax(tax); tax.item_wise_tax_detail = {}; let tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]; @@ -1101,6 +1103,73 @@ frappe.ui.form.on('Payment Entry', { }); }, + validate_taxes_and_charges: function(d) { + let msg = ""; + + if (d.account_head && !d.description) { + // set description from account head + d.description = d.account_head.split(' - ').slice(0, -1).join(' - '); + } + + if (!d.charge_type && (d.row_id || d.rate || d.tax_amount)) { + msg = __("Please select Charge Type first"); + d.row_id = ""; + d.rate = d.tax_amount = 0.0; + } else if ((d.charge_type == 'Actual' || d.charge_type == 'On Net Total' || d.charge_type == 'On Paid Amount') && d.row_id) { + msg = __("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"); + d.row_id = ""; + } else if ((d.charge_type == 'On Previous Row Amount' || d.charge_type == 'On Previous Row Total') && d.row_id) { + if (d.idx == 1) { + msg = __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"); + d.charge_type = ''; + } else if (!d.row_id) { + msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]); + d.row_id = ""; + } else if (d.row_id && d.row_id >= d.idx) { + msg = __("Cannot refer row number greater than or equal to current row number for this Charge type"); + d.row_id = ""; + } + } + if (msg) { + frappe.validated = false; + refresh_field("taxes"); + frappe.throw(msg); + } + + }, + + validate_inclusive_tax: function(tax) { + let actual_type_error = function() { + let msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx]) + frappe.throw(msg); + }; + + let on_previous_row_error = function(row_range) { + let msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included", + [tax.idx, __(tax.doctype), tax.charge_type, row_range]) + frappe.throw(msg); + }; + + if(cint(tax.included_in_paid_amount)) { + if(tax.charge_type == "Actual") { + // inclusive tax cannot be of type Actual + actual_type_error(); + } else if(tax.charge_type == "On Previous Row Amount" && + !cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_paid_amount) + ) { + // referred row should also be an inclusive tax + on_previous_row_error(tax.row_id); + } else if(tax.charge_type == "On Previous Row Total") { + let taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id), + function(t) { return cint(t.included_in_paid_amount) ? null : t; }); + if(taxes_not_included.length > 0) { + // all rows above this tax should be inclusive + on_previous_row_error(tax.row_id == 1 ? "1" : "1 - " + tax.row_id); + } + } + } + }, + determine_exclusive_rate: function(frm) { let has_inclusive_tax = false; $.each(frm.doc["taxes"] || [], function(i, row) { @@ -1110,8 +1179,7 @@ frappe.ui.form.on('Payment Entry', { let cumulated_tax_fraction = 0.0; $.each(frm.doc["taxes"] || [], function(i, tax) { - let current_tax_fraction = frm.events.get_current_tax_fraction(frm, tax); - tax.tax_fraction_for_current_item = current_tax_fraction[0]; + tax.tax_fraction_for_current_item = frm.events.get_current_tax_fraction(frm, tax); if(i==0) { tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item; @@ -1132,9 +1200,7 @@ frappe.ui.form.on('Payment Entry', { if(cint(tax.included_in_paid_amount)) { let tax_rate = tax.rate; - if (tax.charge_type == "Actual") { - current_tax_fraction = tax.tax_amount/(frm.doc.paid_amount_after_tax + frm.doc.tax_amount); - } else if(tax.charge_type == "On Paid Amount") { + if(tax.charge_type == "On Paid Amount") { current_tax_fraction = (tax_rate / 100.0); } else if(tax.charge_type == "On Previous Row Amount") { current_tax_fraction = (tax_rate / 100.0) * @@ -1147,7 +1213,6 @@ frappe.ui.form.on('Payment Entry', { if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") { current_tax_fraction *= -1; - inclusive_tax_amount_per_qty *= -1; } return current_tax_fraction; }, @@ -1207,10 +1272,8 @@ frappe.ui.form.on('Payment Entry', { frappe.throw( __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row")); } - if (!tax.row_id) { - tax.row_id = tax.idx - 1; - } } + if(tax.charge_type == "Actual") { current_tax_amount = flt(tax.tax_amount, precision("tax_amount", tax)) } else if(tax.charge_type == "On Paid Amount") { @@ -1296,6 +1359,11 @@ frappe.ui.form.on('Advance Taxes and Charges', { included_in_paid_amount: function(frm) { frm.events.apply_taxes(frm); frm.events.set_unallocated_amount(frm); + }, + + charge_type: function(frm) { + frm.events.apply_taxes(frm); + frm.events.set_unallocated_amount(frm); } }) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2c6deb3896..70b38735e7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -16,9 +16,11 @@ from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_ac from erpnext.controllers.accounts_controller import AccountsController, get_supplier_block_status from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details - from six import string_types, iteritems +from erpnext.controllers.accounts_controller import validate_conversion_rate, \ + validate_taxes_and_charges, validate_inclusive_tax + class InvalidPaymentEntry(ValidationError): pass @@ -407,20 +409,6 @@ class PaymentEntry(AccountsController): net_total = self.paid_amount included_in_paid_amount = 0 - if self.get('references'): - for doc in self.get('references'): - if doc.reference_doctype == 'Purchase Order': - reference_doclist.append(doc.reference_name) - - if reference_doclist: - order_amount = frappe.db.get_all('Purchase Order', fields=['sum(net_total)'], - filters = {'name': ('in', reference_doclist), 'docstatus': 1, - 'apply_tds': 1}, as_list=1) - - if order_amount: - net_total = order_amount[0][0] - included_in_paid_amount = 1 - # Adding args as purchase invoice to get TDS amount args = frappe._dict({ 'company': self.company, @@ -719,9 +707,9 @@ class PaymentEntry(AccountsController): if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if (self.payment_type == 'Pay' and self.advance_tax_account) or self.payment_type == 'Receive': + if self.payment_type == 'Pay': dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" - elif (self.payment_type == 'Receive' and self.advance_tax_account) or self.payment_type == 'Pay': + elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" payment_or_advance_account = self.get_party_account_for_taxes() @@ -747,6 +735,8 @@ class PaymentEntry(AccountsController): if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, + "party_type": self.party_type, + "party": self.party }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -770,9 +760,9 @@ class PaymentEntry(AccountsController): def get_party_account_for_taxes(self): if self.advance_tax_account: return self.advance_tax_account - elif self.payment_type == 'Pay': - return self.paid_from elif self.payment_type == 'Receive': + return self.paid_from + elif self.payment_type == 'Pay': return self.paid_to def update_advance_paid(self): @@ -823,6 +813,9 @@ class PaymentEntry(AccountsController): def initialize_taxes(self): for tax in self.get("taxes"): + validate_taxes_and_charges(tax) + validate_inclusive_tax(tax, self) + tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] if tax.charge_type != "Actual": @@ -918,15 +911,11 @@ class PaymentEntry(AccountsController): if cint(tax.included_in_paid_amount): tax_rate = tax.rate - if tax.charge_type == 'Actual': - current_tax_fraction = tax.tax_amount/ (self.paid_amount_after_tax + tax.tax_amount) - elif tax.charge_type == "On Paid Amount": + if tax.charge_type == "On Paid Amount": current_tax_fraction = tax_rate / 100.0 - elif tax.charge_type == "On Previous Row Amount": current_tax_fraction = (tax_rate / 100.0) * \ self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item - elif tax.charge_type == "On Previous Row Total": current_tax_fraction = (tax_rate / 100.0) * \ self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item @@ -1626,6 +1615,13 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta paid_amount = received_amount * doc.get('conversion_rate', 1) if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) + + if dt == "Purchase Order" and doc.apply_tds: + if party_account_currency == bank.account_currency: + paid_amount = received_amount = doc.base_net_total + else: + paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) + return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 9b07645ccc..1fa68e0a8a 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -218,6 +218,7 @@ }, { "default": "0", + "depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry", "fieldname": "included_in_paid_amount", "fieldtype": "Check", @@ -227,7 +228,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:48:25.335733", + "modified": "2021-06-14 01:43:50.750455", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 170d34e651..1b7a0fe562 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -195,6 +195,7 @@ }, { "default": "0", + "depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry", "fieldname": "included_in_paid_amount", "fieldtype": "Check", @@ -205,7 +206,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:48:04.691596", + "modified": "2021-06-14 01:44:36.899147", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7c6061defa..a507159cb6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1192,7 +1192,7 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c def validate_taxes_and_charges(tax): - if tax.charge_type in ['Actual', 'On Net Total'] and tax.row_id: + if tax.charge_type in ['Actual', 'On Net Total', 'On Paid Amount'] and tax.row_id: frappe.throw(_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'")) elif tax.charge_type in ['On Previous Row Amount', 'On Previous Row Total']: if cint(tax.idx) == 1: @@ -1209,23 +1209,23 @@ def validate_taxes_and_charges(tax): def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): - throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, - row_range)) + throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) - if cint(getattr(tax, "included_in_print_rate", None)): - if tax.charge_type == "Actual": - # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate): - # referred row should also be inclusive - _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]): - # all rows about the reffered tax should be inclusive - _on_previous_row_error("1 - %d" % (tax.row_id,)) - elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + for fieldname in ['included_in_print_rate', 'included_in_paid_amount']: + if cint(getattr(tax, fieldname, None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].get(fieldname)): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.get(fieldname) for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): From 0511ffcf30459a8646978429101c7edf6315f69b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 14 Jun 2021 13:22:44 +0530 Subject: [PATCH 086/550] fix(General Ledger): Implement multi-account selection --- .../report/general_ledger/general_ledger.js | 15 +++++------- .../report/general_ledger/general_ledger.py | 24 ++++++++++++++----- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 84f786814d..f3c3865b4e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -36,16 +36,13 @@ frappe.query_reports["General Ledger"] = { { "fieldname":"account", "label": __("Account"), - "fieldtype": "Link", + "fieldtype": "MultiSelectList", "options": "Account", - "get_query": function() { - var company = frappe.query_report.get_filter_value('company'); - return { - "doctype": "Account", - "filters": { - "company": company, - } - } + get_data: function(txt) { + console.log("txt = ", txt) + return frappe.db.get_link_options('Account', txt, { + company: frappe.query_report.get_filter_value("company") + }); } }, { diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 562df4f6f7..53c638bf4a 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -49,8 +49,12 @@ def validate_filters(filters, account_details): if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - if filters.get("account") and not account_details.get(filters.account): - frappe.throw(_("Account {0} does not exists").format(filters.account)) + for account in filters.account: + if not account_details.get(account): + frappe.throw(_("Account {0} does not exists").format(account)) + + if filters.get('account'): + filters.account = frappe.parse_json(filters.get('account')) if (filters.get("account") and filters.get("group_by") == _('Group by Account') and account_details[filters.account].is_group == 0): @@ -87,7 +91,7 @@ def set_account_currency(filters): account_currency = None if filters.get("account"): - account_currency = get_account_currency(filters.account) + account_currency = get_account_currency(filters.account[0]) elif filters.get("party"): gle_currency = frappe.db.get_value( "GL Entry", { @@ -205,10 +209,18 @@ def get_gl_entries(filters, accounting_dimensions): def get_conditions(filters): conditions = [] + if filters.get("account") and not filters.get("include_dimensions"): - lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) - conditions.append("""account in (select name from tabAccount - where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + account_conditions = "" + for account in filters["account"]: + lft, rgt = frappe.db.get_value("Account", account, ["lft", "rgt"]) + account_conditions += """account in (select name from tabAccount + where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt) + + # so that the OR doesn't get added to the last account condition + if account != filters["account"][-1]: + account_conditions += " OR " + conditions.append(account_conditions) if filters.get("cost_center"): filters.cost_center = get_cost_centers_with_children(filters.cost_center) From 8718013c96441a7ab71b22bc97fe9bc06bdb01d7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 14:34:44 +0530 Subject: [PATCH 087/550] fix: Add separate function to validate payment entry taxes --- .../doctype/payment_entry/payment_entry.py | 22 ++++++++++++-- erpnext/controllers/accounts_controller.py | 29 +++++++++---------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 70b38735e7..3a1c7b9234 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -18,8 +18,7 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from six import string_types, iteritems -from erpnext.controllers.accounts_controller import validate_conversion_rate, \ - validate_taxes_and_charges, validate_inclusive_tax +from erpnext.controllers.accounts_controller import validate_taxes_and_charges class InvalidPaymentEntry(ValidationError): pass @@ -925,6 +924,25 @@ class PaymentEntry(AccountsController): return current_tax_fraction +def validate_inclusive_tax(tax, doc): + def _on_previous_row_error(row_range): + throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) + + if cint(getattr(tax, "included_in_paid_amount", None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + @frappe.whitelist() def get_outstanding_reference_documents(args): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a507159cb6..1cd0f5f5b2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1211,21 +1211,20 @@ def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) - for fieldname in ['included_in_print_rate', 'included_in_paid_amount']: - if cint(getattr(tax, fieldname, None)): - if tax.charge_type == "Actual": - # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].get(fieldname)): - # referred row should also be inclusive - _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.get(fieldname) for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): - # all rows about the referred tax should be inclusive - _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) - elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + if cint(getattr(tax, "included_in_print_rate", None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.included_in_print_rate for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): From 9eac4d0af66ab1952e20d0236d675ff09f03657a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 14:44:19 +0530 Subject: [PATCH 088/550] fix: Import throw --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3a1c7b9234..b6b2bef963 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext, json -from frappe import _, scrub, ValidationError +from frappe import _, scrub, ValidationError, throw from frappe.utils import flt, comma_or, nowdate, getdate, cint from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on from erpnext.accounts.party import get_party_account From 3b070d1b0540d320edecceefec2bf51a3e308bb2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 14 Jun 2021 14:51:33 +0530 Subject: [PATCH 089/550] fix: Flaky test for Report Subcontracted Raw materials to be transferred --- ...tracted_raw_materials_to_be_transferred.py | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 11ec7669b0..2448e17c50 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -30,42 +30,54 @@ class TestSubcontractedItemToBeTransferred(unittest.TestCase): po.reload() po_data = [row for row in data if row.get('purchase_order') == po.name] + # Alphabetically sort to be certain of order + po_data = sorted(po_data, key = lambda i: i['rm_item_code']) self.assertEqual(len(po_data), 2) - self.assertIn(po_data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) - self.assertIn(po_data[0]['p_qty'], [9, 18]) - self.assertIn(po_data[0]['transferred_qty'], [1, 2]) + self.assertEqual(po_data[0]['purchase_order'], po.name) - self.assertEqual(data[1]['purchase_order'], po.name) - self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) - self.assertIn(data[1]['p_qty'], [9, 18]) - self.assertIn(data[1]['transferred_qty'], [1, 2]) + self.assertEqual(po_data[0]['rm_item_code'], '_Test Item') + self.assertEqual(po_data[0]['p_qty'], 8) + self.assertEqual(po_data[0]['transferred_qty'], 2) + + self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100') + self.assertEqual(po_data[1]['p_qty'], 19) + self.assertEqual(po_data[1]['transferred_qty'], 1) se.cancel() po.cancel() def transfer_subcontracted_raw_materials(po): + # Order of supplied items fetched in PO is flaky + transfer_qty_map = { + '_Test Item': 2, + '_Test Item Home Desktop 100': 1 + } + + item_1 = po.supplied_items[0].rm_item_code + item_2 = po.supplied_items[1].rm_item_code + rm_item = [ { 'name': po.supplied_items[0].name, - 'item_code': '_Test Item Home Desktop 100', - 'rm_item_code': '_Test Item Home Desktop 100', - 'item_name': '_Test Item Home Desktop 100', - 'qty': 2, + 'item_code': item_1, + 'rm_item_code': item_1, + 'item_name': item_1, + 'qty': transfer_qty_map[item_1], 'warehouse': '_Test Warehouse - _TC', 'rate': 100, - 'amount': 200, + 'amount': 100 * transfer_qty_map[item_1], 'stock_uom': 'Nos' }, { 'name': po.supplied_items[1].name, - 'item_code': '_Test Item', - 'rm_item_code': '_Test Item', - 'item_name': '_Test Item', - 'qty': 1, + 'item_code': item_2, + 'rm_item_code': item_2, + 'item_name': item_2, + 'qty': transfer_qty_map[item_2], 'warehouse': '_Test Warehouse - _TC', 'rate': 100, - 'amount': 100, + 'amount': 100 * transfer_qty_map[item_2], 'stock_uom': 'Nos' } ] From 27ec51f021931c6d9505e73e6852cb6c4bb97f86 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 14 Jun 2021 16:41:56 +0530 Subject: [PATCH 090/550] fix(General Ledger): Filter Cost Center drop-down list by Company --- erpnext/accounts/report/general_ledger/general_ledger.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 84f786814d..80a25c907e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -135,7 +135,9 @@ frappe.query_reports["General Ledger"] = { "label": __("Cost Center"), "fieldtype": "MultiSelectList", get_data: function(txt) { - return frappe.db.get_link_options('Cost Center', txt); + return frappe.db.get_link_options('Cost Center', txt, { + company: frappe.query_report.get_filter_value("company") + }); } }, { From 75b30efb050ed333d519ebad924b4cb188098521 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 14 Jun 2021 16:42:27 +0530 Subject: [PATCH 091/550] fix(General Ledger): Filter Project drop-down list by Company --- erpnext/accounts/report/general_ledger/general_ledger.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 80a25c907e..a8e55307f9 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -145,7 +145,9 @@ frappe.query_reports["General Ledger"] = { "label": __("Project"), "fieldtype": "MultiSelectList", get_data: function(txt) { - return frappe.db.get_link_options('Project', txt); + return frappe.db.get_link_options('Project', txt, { + company: frappe.query_report.get_filter_value("company") + }); } }, { From 4cd0f6ce23f703e2f98baab83ae0b38ac9e26943 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 20:01:04 +0530 Subject: [PATCH 092/550] fix: Revert unintended changes --- erpnext/controllers/accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1cd0f5f5b2..243939b275 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1220,9 +1220,9 @@ def validate_inclusive_tax(tax, doc): # referred row should also be inclusive _on_previous_row_error(tax.row_id) elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_print_rate for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]): # all rows about the referred tax should be inclusive - _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + _on_previous_row_error("1 - %d" % (tax.row_id,)) elif tax.get("category") == "Valuation": frappe.throw(_("Valuation type charges can not be marked as Inclusive")) From d4acf87feb85bf43f4096356e6c6f8d72cb47719 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 15 Jun 2021 10:35:44 +0530 Subject: [PATCH 093/550] fix(Asset Repair): Make Accounting Dimensions section collapsible --- erpnext/assets/doctype/asset_repair/asset_repair.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 522f2874d9..b81d74bd3d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -174,6 +174,7 @@ "fieldtype": "Section Break" }, { + "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions" @@ -239,7 +240,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-21 10:37:35.002238", + "modified": "2021-06-15 10:34:00.839353", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 285bc60684c4e03e34356e990226cbe5b00edd21 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 15 Jun 2021 12:02:07 +0530 Subject: [PATCH 094/550] fix(Asset Repair): Set asset_name as title --- erpnext/assets/doctype/asset_repair/asset_repair.json | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index b81d74bd3d..1a714a7a3c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -241,6 +241,7 @@ "is_submittable": 1, "links": [], "modified": "2021-06-15 10:34:00.839353", + "title_field": "asset_name", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From b5a14911763fa29ad9f300fdd0dd1b88a2e5126f Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 15 Jun 2021 12:44:04 +0530 Subject: [PATCH 095/550] fix: escaped warehouse value for sql query (#26049) --- erpnext/controllers/stock_controller.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9c29b0076b..6a7c9e3d0e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -558,11 +558,8 @@ def future_sle_exists(args): or_conditions = [] for warehouse, items in warehouse_items_map.items(): or_conditions.append( - "warehouse = '{}' and item_code in ({})".format( - warehouse, - ", ".join(frappe.db.escape(item) for item in items) - ) - ) + f"""warehouse = {frappe.db.escape(warehouse)} + and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") return frappe.db.sql(""" select name From 79dc0f0afce6df44e88b223f8db7fb947047c710 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 03:37:11 +0530 Subject: [PATCH 096/550] fix: Remove console.log() --- erpnext/accounts/report/general_ledger/general_ledger.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index f3c3865b4e..28139d14b2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -39,7 +39,6 @@ frappe.query_reports["General Ledger"] = { "fieldtype": "MultiSelectList", "options": "Account", get_data: function(txt) { - console.log("txt = ", txt) return frappe.db.get_link_options('Account', txt, { company: frappe.query_report.get_filter_value("company") }); From 53a9ac44662be877160c5acbbafc9fcaaa16bf21 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 04:10:29 +0530 Subject: [PATCH 097/550] fix(General Ledger): Condense account_conditions --- erpnext/accounts/report/general_ledger/general_ledger.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 53c638bf4a..aed9c4a049 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -211,15 +211,16 @@ def get_conditions(filters): conditions = [] if filters.get("account") and not filters.get("include_dimensions"): - account_conditions = "" + account_conditions = "account in (select name from tabAccount where" for account in filters["account"]: lft, rgt = frappe.db.get_value("Account", account, ["lft", "rgt"]) - account_conditions += """account in (select name from tabAccount - where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt) + account_conditions += """ (lft>=%s and rgt<=%s) """ % (lft, rgt) # so that the OR doesn't get added to the last account condition if account != filters["account"][-1]: - account_conditions += " OR " + account_conditions += "OR" + + account_conditions += "and docstatus<2)" conditions.append(account_conditions) if filters.get("cost_center"): From 14ea9ebcb7cc34cc386c37e7f2cdc4b60a9ffd58 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 07:03:32 +0530 Subject: [PATCH 098/550] fix(Asset Repair): Display fields according to the state of the doc --- erpnext/assets/doctype/asset_repair/asset_repair.js | 2 -- erpnext/assets/doctype/asset_repair/asset_repair.json | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 7633a595a2..fdb8d0af67 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -3,8 +3,6 @@ frappe.ui.form.on('Asset Repair', { refresh: function(frm) { - frm.toggle_display(['completion_date', 'repair_status', 'accounting_details', 'accounting_dimensions_section'], !(frm.doc.__islocal)); - if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { frappe.route_options = { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 1a714a7a3c..c2780a13b9 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -71,6 +71,7 @@ }, { "allow_on_submit": 1, + "depends_on": "eval:!doc.__islocal", "fieldname": "completion_date", "fieldtype": "Datetime", "label": "Completion Date" @@ -78,6 +79,7 @@ { "allow_on_submit": 1, "default": "Pending", + "depends_on": "eval:!doc.__islocal", "fieldname": "repair_status", "fieldtype": "Select", "label": "Repair Status", @@ -154,6 +156,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.__islocal", "fieldname": "capitalize_repair_cost", "fieldtype": "Check", "label": "Capitalize Repair Cost" @@ -197,6 +200,7 @@ }, { "default": "0", + "depends_on": "eval:!doc.__islocal", "fieldname": "stock_consumption", "fieldtype": "Check", "label": "Stock Consumed During Repair" @@ -231,6 +235,7 @@ "label": "Increase In Asset Life(Months)" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", @@ -240,8 +245,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-15 10:34:00.839353", - "title_field": "asset_name", + "modified": "2021-06-16 07:01:28.217619", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", @@ -280,6 +284,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "asset_name", "track_changes": 1, "track_seen": 1 } \ No newline at end of file From 695cd7099458a571f4beb086be7043a34672b8a5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 07:50:03 +0530 Subject: [PATCH 099/550] fix(Asset Repair): Add title to error messages --- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 8724d0a125..bdc21a7b7b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -55,9 +55,9 @@ class AssetRepair(Document): def check_for_stock_items_and_warehouse(self): if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Repair.")) + frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) if not self.warehouse: - frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Repair were taken.")) + frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def check_for_cost_center(self): if not self.cost_center: From bf52f558705153098195f01b5bd786aea3337b2a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 07:56:40 +0530 Subject: [PATCH 100/550] fix(Asset Repair): Make Stock Items and Warehouse mandatory if stock_consumption is checked --- erpnext/assets/doctype/asset_repair/asset_repair.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index c2780a13b9..253321ad1e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -170,6 +170,7 @@ "fieldname": "stock_items", "fieldtype": "Table", "label": "Stock Items", + "mandatory_depends_on": "stock_consumption", "options": "Stock Item" }, { @@ -218,6 +219,7 @@ "label": "Total Repair Cost" }, { + "depends_on": "stock_consumption", "fieldname": "warehouse", "fieldtype": "Link", "label": "Warehouse", @@ -245,7 +247,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 07:01:28.217619", + "modified": "2021-06-16 07:52:49.438800", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 9e07b7d4a736e324ad50f7053039d10c97fabd3c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:07:06 +0530 Subject: [PATCH 101/550] fix(Asset Repair): Add Company field --- .../assets/doctype/asset_repair/asset_repair.json | 10 +++++++++- erpnext/assets/doctype/asset_repair/asset_repair.py | 12 +++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 253321ad1e..5cc236393f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -11,6 +11,7 @@ "naming_series", "column_break_2", "asset_name", + "company", "section_break_5", "failure_date", "repair_status", @@ -242,12 +243,19 @@ "fieldtype": "Link", "label": "Purchase Invoice", "options": "Purchase Invoice" + }, + { + "fetch_from": "asset.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 07:52:49.438800", + "modified": "2021-06-16 08:02:34.782990", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index bdc21a7b7b..d84810590d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -76,7 +76,7 @@ class AssetRepair(Document): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", "stock_entry_type": "Material Issue", - "company": frappe.get_value('Asset', self.asset, "company") + "company": self.company }) for stock_item in self.stock_items: @@ -103,8 +103,7 @@ class AssetRepair(Document): def get_gl_entries(self): gl_entry = [] - company = frappe.db.get_value('Asset', self.asset, 'company') - repair_and_maintenance_account = frappe.db.get_value('Company', company, 'repair_and_maintenance_account') + repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') fixed_asset_account = self.get_fixed_asset_account() expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account @@ -118,7 +117,7 @@ class AssetRepair(Document): "voucher_no": self.name, "cost_center": self.cost_center, "posting_date": getdate(), - "company": company + "company": self.company }) gl_entry.insert() gl_entry = frappe.get_doc({ @@ -133,15 +132,14 @@ class AssetRepair(Document): "posting_date": getdate(), "against_voucher_type": "Purchase Invoice", "against_voucher": self.purchase_invoice, - "company": company + "company": self.company }) gl_entry.insert() def get_fixed_asset_account(self): asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) - company = frappe.db.get_value('Asset', self.asset, 'company') for account in asset_category.accounts: - if account.company_name == company: + if account.company_name == self.company: return account.fixed_asset_account def modify_depreciation_schedule(self): From 71c60f75d7f45abd16cac637ab64cc8ecd0322a8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:13:36 +0530 Subject: [PATCH 102/550] fix(Asset Repair): Filter Cost Center and Project by Company --- .../doctype/asset_repair/asset_repair.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index fdb8d0af67..1e87722179 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -30,3 +30,21 @@ frappe.ui.form.on('Asset Repair', { } } }); + +cur_frm.fields_dict.cost_center.get_query = function(doc) { + return{ + filters:{ + 'is_group': 0, + 'company': doc.company + } + } +} + +cur_frm.fields_dict.project.get_query = function(doc) { + return{ + filters:{ + 'is_group': 0, + 'company': doc.company + } + } +} \ No newline at end of file From 94ac52c47de45273570aa09c3c56a82084ce076b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:18:45 +0530 Subject: [PATCH 103/550] fix(Asset Repair): Add mandatory_depends_on condition for Purchase Invoice --- erpnext/assets/doctype/asset_repair/asset_repair.json | 3 ++- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 5cc236393f..b2aac7a4e6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -242,6 +242,7 @@ "fieldname": "purchase_invoice", "fieldtype": "Link", "label": "Purchase Invoice", + "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0", "options": "Purchase Invoice" }, { @@ -255,7 +256,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 08:02:34.782990", + "modified": "2021-06-16 08:16:07.581813", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d84810590d..39f7ee2153 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -44,7 +44,6 @@ class AssetRepair(Document): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() if self.capitalize_repair_cost: - self.check_for_purchase_invoice() self.make_gl_entries() if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation'): self.modify_depreciation_schedule() @@ -89,10 +88,6 @@ class AssetRepair(Document): stock_entry.insert() stock_entry.submit() - def check_for_purchase_invoice(self): - if not self.purchase_invoice: - frappe.throw(_("Please link Purchase Invoice.")) - def on_cancel(self): self.make_gl_entries(cancel=True) From c61bbc5915c02515d053534cb831afdc4e8af13a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:26:02 +0530 Subject: [PATCH 104/550] fix(Asset Repair): Use existing function from asset.py for fetching fixed_asset_account --- erpnext/assets/doctype/asset_repair/asset_repair.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 39f7ee2153..443c0a78f5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, cint from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.assets.doctype.asset.asset import get_asset_account class AssetRepair(Document): def validate(self): @@ -99,7 +100,7 @@ class AssetRepair(Document): def get_gl_entries(self): gl_entry = [] repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') - fixed_asset_account = self.get_fixed_asset_account() + fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account gl_entry = frappe.get_doc({ @@ -131,12 +132,6 @@ class AssetRepair(Document): }) gl_entry.insert() - def get_fixed_asset_account(self): - asset_category = frappe.get_doc('Asset Category', frappe.db.get_value('Asset', self.asset, 'asset_category')) - for account in asset_category.accounts: - if account.company_name == self.company: - return account.fixed_asset_account - def modify_depreciation_schedule(self): if self.increase_in_asset_life: asset = frappe.get_doc('Asset', self.asset) From abb0c769a4bf1a4b7c83b78f874f47e9f501ff65 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:33:05 +0530 Subject: [PATCH 105/550] fix(Asset Repair): Uncheck allow_on_submit for all fields --- erpnext/assets/doctype/asset_repair/asset_repair.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index b2aac7a4e6..7e9587428b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -71,14 +71,12 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "depends_on": "eval:!doc.__islocal", "fieldname": "completion_date", "fieldtype": "Datetime", "label": "Completion Date" }, { - "allow_on_submit": 1, "default": "Pending", "depends_on": "eval:!doc.__islocal", "fieldname": "repair_status", @@ -104,13 +102,11 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "fieldname": "actions_performed", "fieldtype": "Long Text", "label": "Actions performed" }, { - "allow_on_submit": 1, "fieldname": "downtime", "fieldtype": "Data", "in_list_view": 1, @@ -122,7 +118,6 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "fieldname": "repair_cost", "fieldtype": "Currency", "label": "Repair Cost" @@ -256,7 +251,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 08:16:07.581813", + "modified": "2021-06-16 08:32:06.160615", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From a33e751b0f4dcb773586c88dfd3f025cb6a62995 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:35:50 +0530 Subject: [PATCH 106/550] fix(Asset Repair): Make Cost Center non-mandatory --- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 443c0a78f5..688f54aee7 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -37,7 +37,6 @@ class AssetRepair(Document): def on_submit(self): self.check_repair_status() - self.check_for_cost_center() if self.stock_consumption or self.capitalize_repair_cost: self.increase_asset_value() @@ -59,10 +58,6 @@ class AssetRepair(Document): if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) - def check_for_cost_center(self): - if not self.cost_center: - frappe.throw(_("Please enter Cost Center.")) - def increase_asset_value(self): asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') for item in self.stock_items: From 491763fa277bb1af5241f62dedd1bd7374af7ac6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 08:45:54 +0530 Subject: [PATCH 107/550] fix(Asset Repair): Fix Sider issues --- .../assets/doctype/asset_repair/asset_repair.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 1e87722179..2319b069b0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -32,19 +32,19 @@ frappe.ui.form.on('Asset Repair', { }); cur_frm.fields_dict.cost_center.get_query = function(doc) { - return{ - filters:{ + return { + filters: { 'is_group': 0, 'company': doc.company } - } -} + }; +}; cur_frm.fields_dict.project.get_query = function(doc) { - return{ - filters:{ + return { + filters: { 'is_group': 0, 'company': doc.company } - } -} \ No newline at end of file + }; +}; \ No newline at end of file From 6d6aee29e835dfa46b85b4c3a75deb5c892b9223 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 10:42:37 +0530 Subject: [PATCH 108/550] fix(Asset Repair): Fix GL Entry creation --- .../doctype/asset_repair/asset_repair.py | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 688f54aee7..ccf8d5ca39 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -9,8 +9,9 @@ from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, from frappe.model.document import Document from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account +from erpnext.controllers.accounts_controller import AccountsController -class AssetRepair(Document): +class AssetRepair(AccountsController): def validate(self): if self.repair_status == "Completed" and not self.completion_date: self.completion_date = nowdate() @@ -93,39 +94,56 @@ class AssetRepair(Document): make_gl_entries(gl_entries, cancel) def get_gl_entries(self): - gl_entry = [] + gl_entries = [] repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account') fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company) expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": expense_account, - "credit": self.total_repair_cost, - "credit_in_account_currency": self.total_repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company - }) - gl_entry.insert() - gl_entry = frappe.get_doc({ - "doctype": "GL Entry", - "account": fixed_asset_account, - "debit": self.total_repair_cost, - "debit_in_account_currency": self.total_repair_cost, - "against": expense_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice, - "company": self.company - }) - gl_entry.insert() + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "credit": self.repair_cost, + "credit_in_account_currency": self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) + + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "credit": self.total_repair_cost - self.repair_cost, + "credit_in_account_currency": self.total_repair_cost - self.repair_cost, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) + + gl_entries.append( + self.get_gl_dict({ + "account": fixed_asset_account, + "debit": self.total_repair_cost, + "debit_in_account_currency": self.total_repair_cost, + "against": expense_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "against_voucher_type": "Purchase Invoice", + "against_voucher": self.purchase_invoice, + "company": self.company + }, item=self) + ) + + return gl_entries def modify_depreciation_schedule(self): if self.increase_in_asset_life: From 34997789cdd2e328cafa1883c6a6b0fa408c5694 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 10:48:07 +0530 Subject: [PATCH 109/550] fix(Asset Repair): Filter Warehouse by Company --- erpnext/assets/doctype/asset_repair/asset_repair.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 2319b069b0..efa6a9d494 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -41,6 +41,14 @@ cur_frm.fields_dict.cost_center.get_query = function(doc) { }; cur_frm.fields_dict.project.get_query = function(doc) { + return { + filters: { + 'company': doc.company + } + }; +}; + +cur_frm.fields_dict.warehouse.get_query = function(doc) { return { filters: { 'is_group': 0, From 1fd80992d705f55215a89a89a90e445224937e1b Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 16 Jun 2021 13:27:34 +0530 Subject: [PATCH 110/550] fix(pos): 'NoneType' object is not iterable (#26066) --- erpnext/selling/page/point_of_sale/point_of_sale.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 7742f24385..296c8c2fd9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -9,7 +9,7 @@ from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability def search_by_term(search_term, warehouse, price_list): - result = search_for_serial_or_batch_or_barcode_number(search_term) + result = search_for_serial_or_batch_or_barcode_number(search_term) or {} item_code = result.get("item_code") or search_term serial_no = result.get("serial_no") or "" @@ -23,9 +23,9 @@ def search_by_term(search_term, warehouse, price_list): item_stock_qty = get_stock_availability(item_code, warehouse) price_list_rate, currency = frappe.db.get_value('Item Price', { - 'price_list': price_list, - 'item_code': item_code - }, ["price_list_rate", "currency"]) + 'price_list': price_list, + 'item_code': item_code + }, ["price_list_rate", "currency"]) or [None, None] item_info.update({ 'serial_no': serial_no, @@ -46,7 +46,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te result = [] if search_term: - result = search_by_term(search_term, warehouse, price_list) + result = search_by_term(search_term, warehouse, price_list) or [] if result: return result From b1c72da7d7511dbdf865f8014984f47c6b6d6849 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 16 Jun 2021 14:08:32 +0530 Subject: [PATCH 111/550] fix: Training event --- .../training_scheduled.json | 4 ++-- .../training_scheduled/training_scheduled.md | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json index e49541e321..f3650038fd 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.json +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json @@ -11,8 +11,8 @@ "event": "Submit", "idx": 0, "is_standard": 1, - "message": "\n \n \n \n \n \n \n \n
\n
\n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
\n
\n\n\n \n \n \n \n \n \n \n
\n
\n {{ doc.introduction }}\n
    \n
  • {{_(\"Event Location\")}}: {{ doc.location }}
  • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
  • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
  • \n
  • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
  • \n {% else %}\n
  • {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n
  • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n {% endif %}\n
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • \n {% if doc.is_mandatory %}\n
  • Note: This Training Event is mandatory
  • \n {% endif %}\n
\n
\n
", - "modified": "2021-05-24 16:29:13.165930", + "message": "\n \n \n \n \n \n \n \n
\n
\n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
\n
\n\n\n \n \n \n \n \n \n \n
\n
\n {{ doc.introduction }}\n
    \n
  • {{_(\"Event Location\")}}: {{ doc.location }}
  • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
  • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
  • \n
  • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
  • \n {% else %}\n
  • \n {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n
  • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}
  • \n {% endif %}\n
  • {{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • \n {% if doc.is_mandatory %}\n
  • {{ _(\"Note: This Training Event is mandatory\") }}
  • \n {% endif %}\n
\n
\n
", + "modified": "2021-06-16 14:08:12.933367", "modified_by": "Administrator", "module": "HR", "name": "Training Scheduled", diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md index 418fd4990e..b9ba846be5 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.md +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md @@ -24,19 +24,19 @@ {% set start = frappe.utils.get_datetime(doc.start_time) %} {% set end = frappe.utils.get_datetime(doc.end_time) %} {% if start.date() == end.date() %} -
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • -
  • - {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} -
  • +
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • +
  • + {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} +
  • {% else %} -
  • {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} -
  • -
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }} -
  • +
  • + {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • +
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }}
  • {% endif %} -
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
  • {{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • {% if doc.is_mandatory %} -
  • Note: This Training Event is mandatory
  • +
  • {{ _("Note: This Training Event is mandatory") }}
  • {% endif %} @@ -44,4 +44,4 @@ - \ No newline at end of file + From 86f689e54aeafc33d2c90e5511fd04cf0458d613 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 14:23:02 +0530 Subject: [PATCH 112/550] fix(General Ledger): Get account_currency accurately --- .../report/general_ledger/general_ledger.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index aed9c4a049..8be6aaf564 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -91,7 +91,19 @@ def set_account_currency(filters): account_currency = None if filters.get("account"): - account_currency = get_account_currency(filters.account[0]) + if len(filters.get("account")) == 1: + account_currency = get_account_currency(filters.account[0]) + else: + currency = get_account_currency(filters.account[0]) + is_same_account_currency = True + for account in filters.get("account"): + if get_account_currency(account) != currency: + is_same_account_currency = False + break + + if is_same_account_currency: + account_currency = currency + elif filters.get("party"): gle_currency = frappe.db.get_value( "GL Entry", { From 60ce00531d0028741044bf7ed92b3feded139126 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 16 Jun 2021 14:25:55 +0530 Subject: [PATCH 113/550] fix: label for enabling ledger posting of change amount (#26070) --- .../doctype/accounts_settings/accounts_settings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 2735b1ccee..0ff7230e55 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -257,9 +257,10 @@ }, { "default": "1", + "description": "If enabled, ledger entries will be posted for change amount in POS transactions", "fieldname": "post_change_gl_entries", "fieldtype": "Check", - "label": "Post Ledger Entries for Given Change" + "label": "Change Ledger Entries for Change Amount" } ], "icon": "icon-cog", @@ -267,7 +268,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-05-25 12:34:05.858669", + "modified": "2021-06-16 13:14:45.739107", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 8c73f6f19e90e1be54d17a3e1f88e7ed535bef90 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:28:26 +0530 Subject: [PATCH 114/550] fix(pos): pos loyalty card alignment (#26051) --- erpnext/public/scss/point-of-sale.scss | 10 +++++++++- erpnext/selling/page/point_of_sale/pos_payment.js | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index 9bdaa8d1ee..c77b2ce3df 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -806,6 +806,9 @@ display: none; float: right; font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } > .cash-shortcuts { @@ -829,6 +832,11 @@ } } } + + > .loyalty-card { + display: flex; + flex-direction: column; + } } } @@ -1134,4 +1142,4 @@ } } } -} \ No newline at end of file +} diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 156fb777fe..c484873d3e 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -481,7 +481,7 @@ erpnext.PointOfSale.Payment = class { const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : ''; this.$payment_modes.append( `
    -
    +
    Redeem Loyalty Points
    ${amount}
    ${loyalty_program}
    @@ -563,4 +563,4 @@ erpnext.PointOfSale.Payment = class { toggle_component(show) { show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); } -}; \ No newline at end of file +}; From 3b1b4684ba37b950657fe32d44001738c4438df9 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:30:45 +0530 Subject: [PATCH 115/550] fix: check for duplicate payment terms in Payment Term Template (#26003) --- .../doctype/payment_terms_template/payment_terms_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index 80e3348d81..39627eb376 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document): def check_duplicate_terms(self): terms = [] for term in self.terms: - term_info = (term.credit_days, term.credit_months, term.due_date_based_on) + term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) if term_info in terms: frappe.msgprint( _('The Payment Term at row {0} is possibly a duplicate.').format(term.idx), From 41b7c1aec0ae507430a6aa433c0c52b91a1ff92e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 14:40:14 +0530 Subject: [PATCH 116/550] fix(General Ledger): Improve account filter --- .../report/general_ledger/general_ledger.py | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 8be6aaf564..03808c3640 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -223,17 +223,8 @@ def get_conditions(filters): conditions = [] if filters.get("account") and not filters.get("include_dimensions"): - account_conditions = "account in (select name from tabAccount where" - for account in filters["account"]: - lft, rgt = frappe.db.get_value("Account", account, ["lft", "rgt"]) - account_conditions += """ (lft>=%s and rgt<=%s) """ % (lft, rgt) - - # so that the OR doesn't get added to the last account condition - if account != filters["account"][-1]: - account_conditions += "OR" - - account_conditions += "and docstatus<2)" - conditions.append(account_conditions) + filters.account = get_accounts_with_children(filters.account) + conditions.append("account in %(account)s") if filters.get("cost_center"): filters.cost_center = get_cost_centers_with_children(filters.cost_center) @@ -291,6 +282,20 @@ def get_conditions(filters): return "and {}".format(" and ".join(conditions)) if conditions else "" +def get_accounts_with_children(accounts): + if not isinstance(accounts, list): + accounts = [d.strip() for d in accounts.strip().split(',') if d] + + all_accounts = [] + for d in accounts: + if frappe.db.exists("Account", d): + lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"]) + children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]}) + all_accounts += [c.name for c in children] + else: + frappe.throw(_("Account: {0} does not exist").format(d)) + + return list(set(all_accounts)) def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] From 6f9de8c86fbabd0498609ed3d645c89c902c8ba3 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 16 Jun 2021 20:01:29 +0530 Subject: [PATCH 117/550] fix: removed extra space from label rate --- .../doctype/purchase_invoice_item/purchase_invoice_item.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 10e1c73ea9..8a55ff87e3 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -272,7 +272,7 @@ "fieldname": "rate", "fieldtype": "Currency", "in_list_view": 1, - "label": "Rate ", + "label": "Rate", "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency", @@ -854,7 +854,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-03-30 09:02:39.256602", + "modified": "2021-06-16 19:57:03.101571", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", From 5c19a9251f864449fed77dd403839d865f0c547d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Jun 2021 22:14:29 +0530 Subject: [PATCH 118/550] fix: Accouting Dimensions for payroll entry accrual Journal Entry --- .../doctype/payroll_entry/payroll_entry.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 7a70679db4..697d2f6167 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -11,6 +11,7 @@ from frappe import _ from erpnext.accounts.utils import get_fiscal_year from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from frappe.desk.reportview import get_match_cond, get_filters_cond +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions class PayrollEntry(Document): def onload(self): @@ -211,7 +212,7 @@ class PayrollEntry(Document): return account_dict def make_accrual_jv_entry(self): - self.check_permission('write') + self.check_permission("write") earnings = self.get_salary_component_total(component_type = "earnings") or {} deductions = self.get_salary_component_total(component_type = "deductions") or {} payroll_payable_account = self.payroll_payable_account @@ -219,12 +220,13 @@ class PayrollEntry(Document): precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") if earnings or deductions: - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Journal Entry' - journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\ + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Journal Entry" + journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\ .format(self.start_date, self.end_date) journal_entry.company = self.company journal_entry.posting_date = self.posting_date + accounting_dimensions = get_accounting_dimensions() or [] accounts = [] currencies = [] @@ -236,37 +238,34 @@ class PayrollEntry(Document): for acc_cc, amount in earnings.items(): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount += flt(amount, precision) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": acc_cc[0], "debit_in_account_currency": flt(amt, precision), "exchange_rate": flt(exchange_rate), - "party_type": '', "cost_center": acc_cc[1] or self.cost_center, "project": self.project - }) + }, accounting_dimensions)) # Deductions for acc_cc, amount in deductions.items(): exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) payable_amount -= flt(amount, precision) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": acc_cc[0], "credit_in_account_currency": flt(amt, precision), "exchange_rate": flt(exchange_rate), "cost_center": acc_cc[1] or self.cost_center, - "party_type": '', "project": self.project - }) + }, accounting_dimensions)) # Payable amount exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": payroll_payable_account, "credit_in_account_currency": flt(payable_amt, precision), "exchange_rate": flt(exchange_rate), - "party_type": '', "cost_center": self.cost_center - }) + }, accounting_dimensions)) journal_entry.set("accounts", accounts) if len(currencies) > 1: @@ -286,6 +285,12 @@ class PayrollEntry(Document): return jv_name + def update_accounting_dimensions(self, row, accounting_dimensions): + for dimension in accounting_dimensions: + row.update({ dimension: self.get(dimension)}) + + return row + def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): conversion_rate = 1 exchange_rate = self.exchange_rate From 66d4e2ba51baada6ba855a04b1452d6d53f601e3 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 17 Jun 2021 08:28:19 +0530 Subject: [PATCH 119/550] fix(Asset Repair): Display value_after_depreciation in Finance Books --- .../assets/doctype/asset_finance_book/asset_finance_book.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index d9b7b695f7..ee3a2072f0 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -67,7 +67,6 @@ { "fieldname": "value_after_depreciation", "fieldtype": "Currency", - "hidden": 1, "label": "Value After Depreciation", "no_copy": 1, "options": "Company:company:default_currency", @@ -85,7 +84,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-05 16:30:09.213479", + "modified": "2021-06-17 08:02:32.650738", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", From 510077b3d4743297d290205c555cfeb239faa50b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Jun 2021 11:21:21 +0530 Subject: [PATCH 120/550] fix(minor): Translation and linting issues --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 697d2f6167..e71d81f323 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -42,7 +42,7 @@ class PayrollEntry(Document): emp_with_sal_slip.append(employee_details.employee) if len(emp_with_sal_slip): - frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip))) + frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` @@ -287,7 +287,7 @@ class PayrollEntry(Document): def update_accounting_dimensions(self, row, accounting_dimensions): for dimension in accounting_dimensions: - row.update({ dimension: self.get(dimension)}) + row.update({dimension: self.get(dimension)}) return row From 05c70ac5849b15d953974f751c6adebdf6bfb51c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 17 Jun 2021 13:02:02 +0530 Subject: [PATCH 121/550] fix(Asset): Replace asset_value with value_after_depreciation in Finance Books --- erpnext/assets/doctype/asset/asset.json | 9 +-------- erpnext/assets/doctype/asset/asset.py | 11 ++++++----- .../asset_finance_book/asset_finance_book.json | 2 +- .../assets/doctype/asset_repair/asset_repair.py | 16 +++++++++++----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 8a0e3ad2a6..d55258c8f6 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -23,7 +23,6 @@ "asset_name", "asset_category", "location", - "asset_value", "custodian", "department", "disposal_date", @@ -484,12 +483,6 @@ "fieldtype": "Section Break", "label": "Finance Books" }, - { - "fieldname": "asset_value", - "fieldtype": "Currency", - "label": "Asset Value", - "read_only": 1 - }, { "fieldname": "to_date", "fieldtype": "Date", @@ -523,7 +516,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-05-21 12:05:29.424083", + "modified": "2021-06-17 12:59:39.189106", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ade74e6055..f67266bb92 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -96,9 +96,6 @@ class Asset(AccountsController): finance_books = get_item_details(self.item_code, self.asset_category) self.set('finance_books', finance_books) - if not(self.asset_value): - self.asset_value = self.gross_purchase_amount - def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -187,8 +184,12 @@ class Asset(AccountsController): start = n break - value_after_depreciation = (flt(self.asset_value) - - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + if d.value_after_depreciation: + value_after_depreciation = (flt(d.value_after_depreciation) - + flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + else: + value_after_depreciation = (flt(self.gross_purchase_amount) - + flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) d.value_after_depreciation = value_after_depreciation diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index ee3a2072f0..e5a5f194c1 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -84,7 +84,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-17 08:02:32.650738", + "modified": "2021-06-17 12:59:05.743683", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ccf8d5ca39..678a47e8c7 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -60,13 +60,19 @@ class AssetRepair(AccountsController): frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def increase_asset_value(self): - asset_value = frappe.db.get_value('Asset', self.asset, 'asset_value') + total_value_of_stock_consumed = 0 for item in self.stock_items: - asset_value += item.total_value + total_value_of_stock_consumed += item.total_value - if self.capitalize_repair_cost: - asset_value += self.repair_cost - frappe.db.set_value('Asset', self.asset, 'asset_value', asset_value) + asset = frappe.get_doc('Asset', self.asset) + asset.flags.ignore_validate_update_after_submit = True + if asset.calculate_depreciation: + for row in asset.finance_books: + row.value_after_depreciation += total_value_of_stock_consumed + + if self.capitalize_repair_cost: + row.value_after_depreciation += self.repair_cost + asset.save() def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ From f9390f596d41004ed7645b967f29fdd9df08e412 Mon Sep 17 00:00:00 2001 From: Alan <2.alan.tom@gmail.com> Date: Thu, 17 Jun 2021 18:13:23 +0530 Subject: [PATCH 122/550] fix: auto unlink warehouse from item on delete (#26073) * fix: auto unlink warehouse from item on delete * fix: sider * refactor: use delete_doc * test: add test for unlinking warehouse from item * refactor: add msgprint to inform user of unlink * refactor: cleanup and reuse extant functions * fix: don't delete row, update table --- .../stock/doctype/warehouse/test_warehouse.py | 34 +++++++++++++++++++ erpnext/stock/doctype/warehouse/warehouse.py | 7 ++++ 2 files changed, 41 insertions(+) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 95478f61f0..e3981c913e 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -11,6 +11,7 @@ from frappe.test_runner import make_test_records import erpnext from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account +from erpnext.stock.doctype.item.test_item import create_item test_records = frappe.get_test_records('Warehouse') @@ -92,6 +93,39 @@ class TestWarehouse(unittest.TestCase): self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": "Test Warehouse for Merging 2 - TCP1"})) + def test_unlinking_warehouse_from_item_defaults(self): + company = "_Test Company" + + warehouse_names = [f'_Test Warehouse {i} for Unlinking' for i in range(2)] + warehouse_ids = [] + for warehouse in warehouse_names: + warehouse_id = create_warehouse(warehouse, company=company) + warehouse_ids.append(warehouse_id) + + item_names = [f'_Test Item {i} for Unlinking' for i in range(2)] + for item, warehouse in zip(item_names, warehouse_ids): + create_item(item, warehouse=warehouse, company=company) + + # Delete warehouses + for warehouse in warehouse_ids: + frappe.delete_doc("Warehouse", warehouse) + + # Check Item existance + for item in item_names: + self.assertTrue( + bool(frappe.db.exists("Item", item)), + f"{item} doesn't exist" + ) + + item_doc = frappe.get_doc("Item", item) + for item_default in item_doc.item_defaults: + self.assertNotIn( + item_default.default_warehouse, + warehouse_ids, + f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}." + ) + + def create_warehouse(warehouse_name, properties=None, company=None): if not company: company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 2062bddc7c..3abc13907c 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -54,6 +54,7 @@ class Warehouse(NestedSet): throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse.")) self.update_nsm_model() + self.unlink_from_items() def check_if_sle_exists(self): return frappe.db.sql("""select name from `tabStock Ledger Entry` @@ -138,6 +139,12 @@ class Warehouse(NestedSet): self.save() return 1 + def unlink_from_items(self): + frappe.db.sql(""" + update `tabItem Default` + set default_warehouse=NULL + where default_warehouse=%s""", self.name) + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if is_root: From ddef85ae97024376915b94d52ad347ba2b9c9c8c Mon Sep 17 00:00:00 2001 From: Eben van Deventer Date: Thu, 17 Jun 2021 15:13:30 +0200 Subject: [PATCH 123/550] fix: Correct South Africa VAT Rate (#25894) On 1 April 2018 South Africa increased the VAT rate from 14% to 15%, this proposed change seeks to update the default parameters for a fresh ERPNext installation. --- erpnext/setup/setup_wizard/data/country_wise_tax.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index ec9a6d6b70..daaa626a81 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1867,7 +1867,7 @@ "South Africa": { "South Africa Tax": { "account_name": "VAT", - "tax_rate": 14.00 + "tax_rate": 15.00 } }, From 59e2e4989b4cdb88d7812161837924ba00a3029d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Jun 2021 19:58:16 +0530 Subject: [PATCH 124/550] fix: Incorrect billed qty in Sales Order analytics --- .../selling/report/sales_order_analysis/sales_order_analysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index f5feb95f1a..8cb24460f7 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -59,7 +59,7 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, - IFNULL(sii.qty, 0) as billed_qty, + IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, (soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount, From 354116142a856c81750942ee6f887c3d529b1731 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 18 Jun 2021 09:53:18 +0530 Subject: [PATCH 125/550] fix(Asset Repair): Create GL Entries for each item in Stock Items --- .../doctype/asset_repair/asset_repair.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 678a47e8c7..3bcf958db6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -119,19 +119,23 @@ class AssetRepair(AccountsController): }, item=self) ) - gl_entries.append( - self.get_gl_dict({ - "account": expense_account, - "credit": self.total_repair_cost - self.repair_cost, - "credit_in_account_currency": self.total_repair_cost - self.repair_cost, - "against": repair_and_maintenance_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), - "company": self.company - }, item=self) - ) + if self.stock_consumption: + # creating GL Entries for each row in Stock Items based on the Stock Entry created for it + stock_entry = frappe.get_last_doc('Stock Entry') + for item in stock_entry.items: + gl_entries.append( + self.get_gl_dict({ + "account": item.expense_account, + "credit": item.amount, + "credit_in_account_currency": item.amount, + "against": repair_and_maintenance_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company + }, item=self) + ) gl_entries.append( self.get_gl_dict({ From 94dfd0e318cf13164a2b6eab5ed7d9cef369467f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 18 Jun 2021 09:59:45 +0530 Subject: [PATCH 126/550] fix(Asset): Add function to clear old depreciation schedule --- erpnext/assets/doctype/asset/asset.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f67266bb92..1cef290a7a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -176,13 +176,8 @@ class Asset(AccountsController): for d in self.get('finance_books'): self.validate_asset_finance_books(d) - - start = 0 - for n in range(len(self.schedules)): - if not self.schedules[n].journal_entry: - del self.schedules[n:] - start = n - break + + start = self.clear_depreciation_schedule() if d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - @@ -296,6 +291,15 @@ class Asset(AccountsController): "finance_book_id": d.idx }) + def clear_depreciation_schedule(self): + start = 0 + for n in range(len(self.schedules)): + if not self.schedules[n].journal_entry: + del self.schedules[n:] + start = n + break + return start + def check_is_pro_rata(self, row): has_pro_rata = False From ef972693861bb2b39071351249292fe1ab136432 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 18 Jun 2021 10:11:53 +0530 Subject: [PATCH 127/550] fix: timeout while cancelling stock reconciliation --- .../doctype/stock_reconciliation/stock_reconciliation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 306df99b3c..2b51c1a5c3 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -473,6 +473,13 @@ class StockReconciliation(StockController): else: self._submit() + def cancel(self): + if len(self.items) > 100: + msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage")) + self.queue_action('cancel', timeout=2000) + else: + self._cancel() + @frappe.whitelist() def get_items(warehouse, posting_date, posting_time, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) From a58b571ccb727ffadded90c1b8b7541bd8c6c8c4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Jun 2021 10:45:35 +0530 Subject: [PATCH 128/550] fix: Billing address not fetched in Purchase Invoice --- .../doctype/purchase_invoice/purchase_invoice.js | 10 +++++----- erpnext/public/js/controllers/transaction.js | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index f58c8f4526..dc9094c3e9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }); }, - company: function() { - erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - }, - onload: function() { this._super(); @@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", { frm: frm, freeze_message: __("Creating Purchase Receipt ...") }) - } + }, + + company: function(frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, }) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 89fed3bf0d..340c0933ef 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -864,9 +864,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } - if (this.frm.doc.posting_date) var date = this.frm.doc.posting_date; - else var date = this.frm.doc.transaction_date; - if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ From b066fe9519726adc26cf7a0a065f4504d8cc6e1d Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:29:07 +0530 Subject: [PATCH 129/550] fix: insufficient permission for dunning error (#26092) --- erpnext/accounts/doctype/dunning/dunning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 1a6dbedf56..c6c689212b 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -86,7 +86,7 @@ def resolve_dunning(doc, state): for reference in doc.references: if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: dunnings = frappe.get_list('Dunning', filters={ - 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}) + 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True) for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') @@ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte grand_total = 0 if rate_of_interest: interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 - interest_amount = (interest_per_year * cint(overdue_days)) / 365 + interest_amount = (interest_per_year * cint(overdue_days)) / 365 grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { From 3d8f82459b0bd90c80aec473f9a0daa5a7564db8 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 18 Jun 2021 11:42:28 +0530 Subject: [PATCH 130/550] fix(Issue): reset response_by and resolution_by if SLA is removed (#25997) --- erpnext/support/doctype/issue/issue.json | 6 +++--- erpnext/support/doctype/issue/issue.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index bc29821ee2..14712f89fe 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -166,7 +166,7 @@ "options": "Service Level Agreement" }, { - "depends_on": "eval: doc.status != 'Replied';", + "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;", "fieldname": "response_by", "fieldtype": "Datetime", "label": "Response By", @@ -180,7 +180,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.status != 'Replied';", + "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;", "fieldname": "resolution_by", "fieldtype": "Datetime", "label": "Resolution By", @@ -410,7 +410,7 @@ "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2021-05-26 10:49:07.574769", + "modified": "2021-06-10 03:22:27.098898", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index b068363f06..9c69deb6a4 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -29,6 +29,9 @@ class Issue(Document): self.update_status() self.set_lead_contact(self.raised_by) + if not self.service_level_agreement: + self.reset_sla_fields() + def on_update(self): # Add a communication in the issue timeline if self.flags.create_communication and self.via_customer_portal: @@ -54,6 +57,13 @@ class Issue(Document): self.company = frappe.db.get_value("Lead", self.lead, "company") or \ frappe.db.get_default("Company") + def reset_sla_fields(self): + self.agreement_status = "" + self.response_by = "" + self.resolution_by = "" + self.response_by_variance = 0 + self.resolution_by_variance = 0 + def update_status(self): status = frappe.db.get_value("Issue", self.name, "status") if self.status != "Open" and status == "Open" and not self.first_responded_on: @@ -511,4 +521,4 @@ def get_time_in_timedelta(time): Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215) """ import datetime - return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) \ No newline at end of file + return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) From 81c97c13ce1b697acc16d38f1e1084f5da573882 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Jun 2021 17:25:37 +0530 Subject: [PATCH 131/550] fix: Sanctioned loan amount limit check --- erpnext/loan_management/doctype/loan/loan.py | 29 +++++- .../loan_management/doctype/loan/test_loan.py | 45 ++++++++- .../loan_application/loan_application.py | 4 +- .../doctype/loan_repayment/loan_repayment.py | 91 ++++++++++--------- 4 files changed, 116 insertions(+), 53 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 69d11a8653..ff7fbbdf49 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -60,8 +60,9 @@ class Loan(AccountsController): self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) def check_sanctioned_amount_limit(self): - total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit: + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) @@ -155,9 +156,29 @@ def update_total_amount_paid(doc): frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) def get_total_loan_amount(applicant_type, applicant, company): - return frappe.db.get_value('Loan', - {'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, - 'sum(loan_amount)') + pending_amount = 0 + loan_details = frappe.db.get_all("Loan", + filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1, + "status": ("!=", "Closed")}, + fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid", + "written_off_amount"]) + + interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type, + "company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)")) + + for loan in loan_details: + if loan.status in ("Disbursed", "Loan Closure Requested"): + pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + elif loan.status == "Partially Disbursed": + pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + elif loan.status == "Sanctioned": + pending_amount += flt(loan.total_payment) + + pending_amount += interest_amount + + return pending_amount def get_sanctioned_amount_limit(applicant_type, applicant, company): return frappe.db.get_value('Sanctioned Loan Amount', diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index fa4707ce2b..314f58dd15 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase): if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) - self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + if not frappe.db.exists("Customer", "_Test Loan Customer 1"): + frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True) + + self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") + self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name") create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) @@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase): self.assertTrue(gl_entries1) self.assertTrue(gl_entries2) + def test_sanctioned_amount_limit(self): + # Clear loan docs before checking + frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'") + frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'") + frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'") + + if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", "company": "_Test Company"}): + frappe.get_doc({ + "doctype": "Sanctioned Loan Amount", + "applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", + "sanctioned_amount_limit": 1500000, + "company": "_Test Company" + }).insert(ignore_permissions=True) + + # Make First Loan + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge) + create_pledge(loan_application) + loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + # Make second loan greater than the sanctioned amount + loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge, + do_not_save=True) + self.assertRaises(frappe.ValidationError, loan_application.save) + def test_regular_loan_repayment(self): pledge = [{ "loan_security": "Test Security 1", @@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase): unpledge_request.load_from_db() self.assertEqual(unpledge_request.docstatus, 1) - def test_santined_loan_security_unpledge(self): + def test_sanctioned_loan_security_unpledge(self): pledge = [{ "loan_security": "Test Security 1", "qty": 4000.00 @@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount): return lr def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, - repayment_periods=None, posting_date=None): + repayment_periods=None, posting_date=None, do_not_save=False): loan_application = frappe.new_doc('Loan Application') loan_application.applicant_type = 'Customer' loan_application.company = company @@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep for pledge in proposed_pledges: loan_application.append('proposed_pledges', pledge) + if do_not_save: + return loan_application + loan_application.save() loan_application.submit() diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index 9c0147e55b..d8f3577b2c 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -46,9 +46,11 @@ class LoanApplication(Document): frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) def check_sanctioned_amount_limit(self): - total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit: + total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 3d99b1f304..b8b1a40b5f 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -235,70 +235,71 @@ class LoanRepayment(AccountsController): else: remarks = _("Repayment against Loan: ") + self.against_loan - if self.total_penalty_paid: + if not loan_details.repay_from_salary: + if self.total_penalty_paid: + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "against": loan_details.payment_account, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.penalty_income_account, + "against": loan_details.payment_account, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + gle_map.append( self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, - "debit": self.total_penalty_paid, - "debit_in_account_currency": self.total_penalty_paid, + "account": loan_details.payment_account, + "against": loan_details.loan_account + ", " + loan_details.interest_income_account + + ", " + loan_details.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) gle_map.append( self.get_gl_dict({ - "account": loan_details.penalty_income_account, + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, "against": loan_details.payment_account, - "credit": self.total_penalty_paid, - "credit_in_account_currency": self.total_penalty_paid, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) ) - gle_map.append( - self.get_gl_dict({ - "account": loan_details.payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, - "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, - "against": loan_details.payment_account, - "credit": self.amount_paid, - "credit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - if gle_map: - make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) def create_repayment_entry(loan, applicant, company, posting_date, loan_type, payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): From 8520edc952d252aa4ebdbf9bf56e2b04a12dd614 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 15 Jun 2021 10:21:44 +0530 Subject: [PATCH 132/550] fix: time out while submitting the stock transactions with more than 50 items --- erpnext/controllers/buying_controller.py | 10 ++- erpnext/controllers/stock_controller.py | 79 ++++++++++++++----- .../stock_ledger_entry/stock_ledger_entry.py | 19 ++--- erpnext/stock/stock_ledger.py | 16 +++- erpnext/stock/utils.py | 2 +- 5 files changed, 93 insertions(+), 33 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index da819119b1..20f5445725 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -171,12 +171,13 @@ class BuyingController(StockController): TODO: rename item_tax_amount to valuation_tax_amount """ + stock_and_asset_items = [] stock_and_asset_items = self.get_stock_items() + self.get_asset_items() stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 last_item_idx = 1 for d in self.get("items"): - if d.item_code and d.item_code in stock_and_asset_items: + if (d.item_code and d.item_code in stock_and_asset_items): stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_amount += flt(d.base_net_amount) last_item_idx = d.idx @@ -683,7 +684,8 @@ class BuyingController(StockController): self.process_fixed_asset() self.update_fixed_asset(field) - update_last_purchase_rate(self, is_submit = 1) + if self.doctype in ['Purchase Order', 'Purchase Receipt']: + update_last_purchase_rate(self, is_submit = 1) def on_cancel(self): super(BuyingController, self).on_cancel() @@ -691,7 +693,9 @@ class BuyingController(StockController): if self.get('is_return'): return - update_last_purchase_rate(self, is_submit = 0) + if self.doctype in ['Purchase Order', 'Purchase Receipt']: + update_last_purchase_rate(self, is_submit = 0) + if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 6a7c9e3d0e..35097b97b9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -501,7 +501,6 @@ class StockController(AccountsController): check_if_stock_and_account_balance_synced(self.posting_date, self.company, self.doctype, self.name) - @frappe.whitelist() def make_quality_inspections(doctype, docname, items): if isinstance(items, str): @@ -533,21 +532,75 @@ def make_quality_inspections(doctype, docname, items): return inspections - def is_reposting_pending(): return frappe.db.exists("Repost Item Valuation", {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) +def future_sle_exists(args, sl_entries=None): + key = (args.voucher_type, args.voucher_no) -def future_sle_exists(args): - sl_entries = frappe.get_all("Stock Ledger Entry", + if validate_future_sle_not_exists(args, key, sl_entries): + return False + elif get_cached_data(args, key): + return True + + if not sl_entries: + sl_entries = get_sle_entries_against_voucher(args) + if not sl_entries: + return + + or_conditions = get_conditions_to_validate_future_sle(sl_entries) + + data = frappe.db.sql(""" + select item_code, warehouse, count(name) as total_row + from `tabStock Ledger Entry` + where + ({}) + and timestamp(posting_date, posting_time) + >= timestamp(%(posting_date)s, %(posting_time)s) + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + GROUP BY + item_code, warehouse + """.format(" or ".join(or_conditions)), args, as_dict=1) + + for d in data: + frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row + + return len(data) + +def validate_future_sle_not_exists(args, key, sl_entries=None): + item_key = '' + if args.get('item_code'): + item_key = (args.get('item_code'), args.get('warehouse')) + + if not sl_entries and hasattr(frappe.local, 'future_sle'): + if (not frappe.local.future_sle.get(key) or + (item_key and item_key not in frappe.local.future_sle.get(key))): + return True + +def get_cached_data(args, key): + if not hasattr(frappe.local, 'future_sle'): + frappe.local.future_sle = {} + + if key not in frappe.local.future_sle: + frappe.local.future_sle[key] = frappe._dict({}) + + if args.get('item_code'): + item_key = (args.get('item_code'), args.get('warehouse')) + count = frappe.local.future_sle[key].get(item_key) + + return True if (count or count == 0) else False + else: + return frappe.local.future_sle[key] + +def get_sle_entries_against_voucher(args): + return frappe.get_all("Stock Ledger Entry", filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, fields=["item_code", "warehouse"], order_by="creation asc") - if not sl_entries: - return - +def get_conditions_to_validate_future_sle(sl_entries): warehouse_items_map = {} for entry in sl_entries: if entry.warehouse not in warehouse_items_map: @@ -561,17 +614,7 @@ def future_sle_exists(args): f"""warehouse = {frappe.db.escape(warehouse)} and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") - return frappe.db.sql(""" - select name - from `tabStock Ledger Entry` - where - ({}) - and timestamp(posting_date, posting_time) - >= timestamp(%(posting_date)s, %(posting_time)s) - and voucher_no != %(voucher_no)s - and is_cancelled = 0 - limit 1 - """.format(" or ".join(or_conditions)), args) + return or_conditions def create_repost_item_valuation_entry(args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index b0e7440e6c..0febcb6891 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, date_diff +from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, cint from frappe.model.document import Document from datetime import date from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock @@ -108,17 +108,18 @@ class StockLedgerEntry(Document): self.stock_uom = item_det.stock_uom def check_stock_frozen_date(self): - stock_frozen_upto = frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto') or '' - if stock_frozen_upto: - stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role') - if getdate(self.posting_date) <= getdate(stock_frozen_upto) and not stock_auth_role in frappe.get_roles(): - frappe.throw(_("Stock transactions before {0} are frozen").format(formatdate(stock_frozen_upto)), StockFreezeError) + stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings') - stock_frozen_upto_days = int(frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto_days') or 0) + if stock_settings.stock_frozen_upto: + if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) + and stock_settings.stock_auth_role not in frappe.get_roles()): + frappe.throw(_("Stock transactions before {0} are frozen") + .format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError) + + stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days) if stock_frozen_upto_days: - stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role') older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()) - if older_than_x_days_ago and not stock_auth_role in frappe.get_roles(): + if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles(): frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError) def scrub_posting_time(self): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fc82c789cc..fb2ecab249 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -22,6 +22,7 @@ _exceptions = frappe.local('stockledger_exceptions') # _exceptions = [] def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): + from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: from erpnext.stock.utils import update_bin @@ -30,6 +31,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc validate_cancellation(sl_entries) set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) + args = get_args_for_future_sle(sl_entries[0]) + future_sle_exists(args, sl_entries) + for sle in sl_entries: if sle.serial_no: validate_serial_no(sle) @@ -53,6 +57,14 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc args = sle_doc.as_dict() update_bin(args, allow_negative_stock, via_landed_cost_voucher) +def get_args_for_future_sle(row): + return frappe._dict({ + 'voucher_type': row.get('voucher_type'), + 'voucher_no': row.get('voucher_no'), + 'posting_date': row.get('posting_date'), + 'posting_time': row.get('posting_time') + }) + def validate_serial_no(sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for sn in get_serial_nos(sle.serial_no): @@ -472,8 +484,8 @@ class update_entries_after(object): frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate) # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice - if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): - doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': + doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) for d in (doc.items + doc.supplied_items): d.db_update() diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 034d3ebbb5..8a6a3a3e4a 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -177,7 +177,7 @@ def get_bin(item_code, warehouse): return bin_obj def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): - is_stock_item = frappe.db.get_value('Item', args.get("item_code"), 'is_stock_item') + is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') if is_stock_item: bin = get_bin(args.get("item_code"), args.get("warehouse")) bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher) From ba288274f21f057765500e62f9ffbaf718913aef Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 19 Jun 2021 12:31:12 +0530 Subject: [PATCH 133/550] fix: ignore permission to update call log --- erpnext/telephony/doctype/call_log/call_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 4d553df08b..c00dfa9056 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -142,7 +142,7 @@ def link_existing_conversations(doc, state): for log in logs: call_log = frappe.get_doc('Call Log', log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) - call_log.save() + call_log.save(ignore_permissions=True) frappe.db.commit() except Exception: frappe.log_error(title=_('Error during caller information update')) From 93b975277179481b9884215d6d15a44d76fdd3d2 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 13:06:27 +0530 Subject: [PATCH 134/550] fix: Add comments --- erpnext/assets/doctype/asset/asset.py | 12 ++++++-- .../doctype/asset_repair/asset_repair.py | 30 +++++++++++-------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 1cef290a7a..9a8b6c9791 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -179,7 +179,8 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() - if d.value_after_depreciation: + # value_after_depreciation - current Asset value + if d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) else: @@ -291,6 +292,7 @@ class Asset(AccountsController): "finance_book_id": d.idx }) + # used when depreciation schedule needs to be modified due to increase in asset life def clear_depreciation_schedule(self): start = 0 for n in range(len(self.schedules)): @@ -300,10 +302,13 @@ class Asset(AccountsController): break return start + + # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False - days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1 + + # if frequency_of_depreciation is 12 months, total_days = 365 total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) if days < total_days: @@ -783,9 +788,12 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) if row.depreciation_method in ("Straight Line", "Manual"): + # if the Depreciation Schedule is being prepared for the first time if not asset.to_date: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left + + # if the Depreciation Schedule is being modified after Asset Repair else: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 3bcf958db6..fb815a2451 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -46,7 +46,7 @@ class AssetRepair(AccountsController): self.decrease_stock_quantity() if self.capitalize_repair_cost: self.make_gl_entries() - if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation'): + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: self.modify_depreciation_schedule() def check_repair_status(self): @@ -156,27 +156,33 @@ class AssetRepair(AccountsController): return gl_entries def modify_depreciation_schedule(self): - if self.increase_in_asset_life: - asset = frappe.get_doc('Asset', self.asset) - asset.flags.ignore_validate_update_after_submit = True - for row in asset.finance_books: - row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation + asset = frappe.get_doc('Asset', self.asset) + asset.flags.ignore_validate_update_after_submit = True + for row in asset.finance_books: + row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation - asset.edit_dates = "" - extra_months = self.increase_in_asset_life % row.frequency_of_depreciation - if extra_months != 0: - self.calculate_last_schedule_date(asset, row, extra_months) + asset.edit_dates = "" + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date(asset, row, extra_months) - asset.prepare_depreciation_data() - asset.save() + asset.prepare_depreciation_data() + asset.save() # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation def calculate_last_schedule_date(self, asset, row, extra_months): asset.edit_dates = "Don't Edit" number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ cint(asset.number_of_depreciations_booked) + + # the Schedule Date in the final row of the old Depreciation Schedule last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + + # the Schedule Date in the final row of the new Depreciation Schedule asset.to_date = add_months(last_schedule_date, extra_months) + + # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations + # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... schedule_date = add_months(row.depreciation_start_date, number_of_pending_depreciations * cint(row.frequency_of_depreciation)) From 2b93e54e1f43e35d2b51ea3b954ef7ad87892844 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 13:45:37 +0530 Subject: [PATCH 135/550] fix(Asset Repair): Fix depreciation_amount calculation --- erpnext/assets/doctype/asset/asset.py | 2 +- erpnext/regional/india/utils.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9a8b6c9791..a2917fe399 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -789,7 +789,7 @@ def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time - if not asset.to_date: + if not asset.edit_dates: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 075c698fea..4d373444a6 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -838,8 +838,16 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked) if row.depreciation_method in ("Straight Line", "Manual"): - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / depreciation_left + # if the Depreciation Schedule is being prepared for the first time + if not asset.edit_dates: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / depreciation_left + + # if the Depreciation Schedule is being modified after Asset Repair + else: + depreciation_amount = (flt(row.value_after_depreciation) - + flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + else: rate_of_depreciation = row.rate_of_depreciation # if its the first depreciation From da8da9fa4e2991029d7c90e9fdd8243360b9f3f4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 14:00:26 +0530 Subject: [PATCH 136/550] fix: Replace edit_dates with flags.increase_in_asset_life --- erpnext/assets/doctype/asset/asset.json | 9 +-------- erpnext/assets/doctype/asset/asset.py | 4 ++-- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++-- erpnext/regional/india/utils.py | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index d55258c8f6..d77eb10418 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -54,7 +54,6 @@ "section_break_14", "schedules", "to_date", - "edit_dates", "insurance_details", "policy_number", "insurer", @@ -488,12 +487,6 @@ "fieldtype": "Date", "hidden": 1, "label": "To Date" - }, - { - "fieldname": "edit_dates", - "fieldtype": "Data", - "hidden": 1, - "label": "Edit Dates" } ], "idx": 72, @@ -516,7 +509,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-06-17 12:59:39.189106", + "modified": "2021-06-19 13:56:58.450182", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a2917fe399..27d21e2542 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -223,7 +223,7 @@ class Asset(AccountsController): # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - if not self.edit_dates: + if not self.flags.increase_in_asset_life: self.to_date = add_months(self.available_for_use_date, n * cint(d.frequency_of_depreciation)) @@ -789,7 +789,7 @@ def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time - if not asset.edit_dates: + if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index fb815a2451..9ef6591499 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -161,7 +161,7 @@ class AssetRepair(AccountsController): for row in asset.finance_books: row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation - asset.edit_dates = "" + asset.flags.increase_in_asset_life = False extra_months = self.increase_in_asset_life % row.frequency_of_depreciation if extra_months != 0: self.calculate_last_schedule_date(asset, row, extra_months) @@ -171,7 +171,7 @@ class AssetRepair(AccountsController): # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation def calculate_last_schedule_date(self, asset, row, extra_months): - asset.edit_dates = "Don't Edit" + asset.flags.increase_in_asset_life = True number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ cint(asset.number_of_depreciations_booked) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 4d373444a6..0dafe01711 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -839,7 +839,7 @@ def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): # if the Depreciation Schedule is being prepared for the first time - if not asset.edit_dates: + if not asset.flags.increase_in_asset_life: depreciation_amount = (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / depreciation_left From 09ba6f64770102520bb5c94f7ce862f9f9752597 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 14:06:45 +0530 Subject: [PATCH 137/550] fix(Asset Repair): Move Total Repair Cost to the Stock Consumption Details section --- erpnext/assets/doctype/asset_repair/asset_repair.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 7e9587428b..f43b5d92bf 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -26,11 +26,11 @@ "capitalize_repair_cost", "stock_consumption", "column_break_8", - "total_repair_cost", "purchase_invoice", "stock_consumption_details_section", "warehouse", "stock_items", + "total_repair_cost", "asset_depreciation_details_section", "increase_in_asset_life", "section_break_9", @@ -210,6 +210,7 @@ }, { "depends_on": "stock_consumption", + "description": "Sum of Repair Cost and the total value of all Stock Items consumed during the repair.", "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost" @@ -251,7 +252,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-16 08:32:06.160615", + "modified": "2021-06-19 14:04:35.423111", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 7db7988e4c08db88c1a9872db29748afa10a1a67 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 14:54:30 +0530 Subject: [PATCH 138/550] fix(Asset Repair): Add Stock Entry field --- .../doctype/asset_repair/asset_repair.json | 16 +++++++++++++--- .../assets/doctype/asset_repair/asset_repair.py | 4 +++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index f43b5d92bf..6c90ca2348 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -31,6 +31,7 @@ "warehouse", "stock_items", "total_repair_cost", + "stock_entry", "asset_depreciation_details_section", "increase_in_asset_life", "section_break_9", @@ -118,6 +119,7 @@ "fieldtype": "Column Break" }, { + "default": "0", "fieldname": "repair_cost", "fieldtype": "Currency", "label": "Repair Cost" @@ -209,11 +211,12 @@ "label": "Stock Consumption Details" }, { - "depends_on": "stock_consumption", + "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0", "description": "Sum of Repair Cost and the total value of all Stock Items consumed during the repair.", "fieldname": "total_repair_cost", "fieldtype": "Currency", - "label": "Total Repair Cost" + "label": "Total Repair Cost", + "read_only": 1 }, { "depends_on": "stock_consumption", @@ -247,12 +250,19 @@ "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "stock_entry", + "fieldtype": "Link", + "label": "Stock Entry", + "options": "Stock Entry", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 14:04:35.423111", + "modified": "2021-06-19 14:47:25.875814", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9ef6591499..212af7a930 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -91,6 +91,8 @@ class AssetRepair(AccountsController): stock_entry.insert() stock_entry.submit() + self.stock_entry = stock_entry.name + def on_cancel(self): self.make_gl_entries(cancel=True) @@ -121,7 +123,7 @@ class AssetRepair(AccountsController): if self.stock_consumption: # creating GL Entries for each row in Stock Items based on the Stock Entry created for it - stock_entry = frappe.get_last_doc('Stock Entry') + stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) for item in stock_entry.items: gl_entries.append( self.get_gl_dict({ From 012b9eaeff8a2729ec1962260f5c05d8fb2fc5c3 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 15:18:54 +0530 Subject: [PATCH 139/550] fix(Asset Repair): Make Error Description non-mandatory --- erpnext/assets/doctype/asset_repair/asset_repair.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 6c90ca2348..53d72ab68f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -95,8 +95,7 @@ { "fieldname": "description", "fieldtype": "Long Text", - "label": "Error Description", - "reqd": 1 + "label": "Error Description" }, { "fieldname": "column_break_9", @@ -262,7 +261,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 14:47:25.875814", + "modified": "2021-06-19 15:18:10.625833", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 23876c085407866ca40e2248d9a6636d5cdff6da Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 19 Jun 2021 15:23:06 +0530 Subject: [PATCH 140/550] fix(Asset Repair): Prevent some fields from being copied on duplicating the doc --- erpnext/assets/doctype/asset_repair/asset_repair.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 53d72ab68f..6f9b6863f7 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -75,7 +75,8 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "completion_date", "fieldtype": "Datetime", - "label": "Completion Date" + "label": "Completion Date", + "no_copy": 1 }, { "default": "Pending", @@ -233,7 +234,8 @@ { "fieldname": "increase_in_asset_life", "fieldtype": "Int", - "label": "Increase In Asset Life(Months)" + "label": "Increase In Asset Life(Months)", + "no_copy": 1 }, { "depends_on": "eval:!doc.__islocal", @@ -241,6 +243,7 @@ "fieldtype": "Link", "label": "Purchase Invoice", "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0", + "no_copy": 1, "options": "Purchase Invoice" }, { @@ -261,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 15:18:10.625833", + "modified": "2021-06-19 15:20:24.056706", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From 8c844e4515afc5f9f241c522578b2e81d19f24b9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 11 Jun 2021 17:27:08 +0530 Subject: [PATCH 141/550] fix: material request and supplier quotation not linked if sq created from supplier portal against rfq --- .../request_for_quotation.py | 22 ++++++++++--------- .../templates/includes/transaction_row.html | 8 ++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 0127eb8163..a4ce84e1cf 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items): create_rfq_items(sq_doc, supplier, data) def create_rfq_items(sq_doc, supplier, data): - sq_doc.append('items', { - "item_code": data.item_code, - "item_name": data.item_name, - "description": data.description, - "qty": data.qty, - "rate": data.rate, - "conversion_factor": data.conversion_factor if data.conversion_factor else None, - "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), - "warehouse": data.warehouse or '', + args = {} + + for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor', + 'warehouse', 'material_request', 'material_request_item', 'stock_qty']: + args[field] = data.get(field) + + args.update({ "request_for_quotation_item": data.name, - "request_for_quotation": data.parent + "request_for_quotation": data.parent, + "supplier_part_no": frappe.db.get_value("Item Supplier", + {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no") }) + sq_doc.append('items', args) + @frappe.whitelist() def get_pdf(doctype, name, supplier): doc = get_rfq_doc(doctype, name, supplier) diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html index 383413103e..3cfb8d8440 100644 --- a/erpnext/templates/includes/transaction_row.html +++ b/erpnext/templates/includes/transaction_row.html @@ -13,9 +13,11 @@ {{ doc.items_preview }}
    -
    - {{ doc.get_formatted("grand_total") }} -
    + {% if doc.get('grand_total') %} +
    + {{ doc.get_formatted("grand_total") }} +
    + {% endif %}
    Link From a94b89727c550cc1eaea86379a2cb4d53297a8b8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 May 2021 20:11:15 +0530 Subject: [PATCH 142/550] feat: subcontract code refactor and enhancement --- .../purchase_invoice/test_purchase_invoice.py | 2 + .../doctype/purchase_order/purchase_order.js | 38 +- .../purchase_order/purchase_order.json | 3 +- .../doctype/purchase_order/purchase_order.py | 59 +- .../purchase_order/test_purchase_order.py | 100 +-- .../purchase_order_item_supplied.json | 45 +- .../purchase_receipt_item_supplied.json | 17 +- .../subcontract_order_summary/__init__.py | 0 .../subcontract_order_summary.js | 45 ++ .../subcontract_order_summary.json | 32 + .../subcontract_order_summary.py | 158 +++++ erpnext/controllers/buying_controller.py | 427 +------------ erpnext/controllers/subcontracting.py | 342 ++++++++++ erpnext/manufacturing/doctype/bom/test_bom.py | 2 + erpnext/stock/doctype/bin/bin.py | 4 +- .../item_alternative/test_item_alternative.py | 5 + .../purchase_receipt/purchase_receipt.js | 2 + .../purchase_receipt/purchase_receipt.json | 5 +- .../purchase_receipt/purchase_receipt.py | 2 + .../purchase_receipt/test_purchase_receipt.py | 4 + .../stock/doctype/stock_entry/stock_entry.js | 4 + .../doctype/stock_entry/stock_entry.json | 15 +- .../stock/doctype/stock_entry/stock_entry.py | 79 ++- erpnext/tests/test_subcontracting.py | 583 ++++++++++++++++++ 24 files changed, 1418 insertions(+), 555 deletions(-) create mode 100644 erpnext/buying/report/subcontract_order_summary/__init__.py create mode 100644 erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js create mode 100644 erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json create mode 100644 erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py create mode 100644 erpnext/controllers/subcontracting.py create mode 100644 erpnext/tests/test_subcontracting.py diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 503dda7728..ff433b962f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(actual_qty_0, get_qty_after_transaction()) def test_subcontracting_via_purchase_invoice(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + update_backflush_based_on('BOM') make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 0f6d927b36..440cde6d9e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -53,6 +53,38 @@ frappe.ui.form.on("Purchase Order", { } else { frm.set_value("tax_withholding_category", frm.supplier_tds); } + }, + + refresh: function(frm) { + frm.trigger('get_materials_from_supplier'); + }, + + get_materials_from_supplier: function(frm) { + let po_details = []; + + if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) { + frm.doc.supplied_items.forEach(d => { + if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + po_details.push(d.name) + } + }); + } + + if (po_details && po_details.length) { + frm.add_custom_button(__('Return of Components'), () => { + frm.call({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier', + freeze_message: __('Creating Stock Entry'), + args: { purchase_order: frm.doc.name, po_details: po_details }, + callback: function(r) { + if (r && r.message) { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); + }, __('Create')); + } } }); @@ -217,7 +249,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( }, has_unsupplied_items: function() { - return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty) + return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty) }, make_stock_entry: function() { @@ -513,12 +545,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( ], primary_action: function() { var data = d.get_values(); + var content_msg = 'Reason for hold: ' + data.reason_for_hold; + frappe.call({ method: "frappe.desk.form.utils.add_comment", args: { reference_doctype: me.frm.doctype, reference_name: me.frm.docname, - content: __('Reason for hold:') + " " +data.reason_for_hold, + content: __(content_msg), comment_email: frappe.session.user, comment_by: frappe.session.user_fullname }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 41668c6291..bb0ad60cab 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -609,6 +609,7 @@ "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", + "no_copy": 1, "oldfieldname": "po_raw_material_details", "oldfieldtype": "Table", "options": "Purchase Order Item Supplied", @@ -1377,7 +1378,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-04-19 00:55:30.781375", + "modified": "2021-05-30 15:17:53.663648", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2629ba7d61..724f863e0f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -503,9 +503,10 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions @frappe.whitelist() def make_rm_stock_entry(purchase_order, rm_items): + rm_items_list = rm_items if isinstance(rm_items, string_types): rm_items_list = json.loads(rm_items) - else: + elif not rm_items: frappe.throw(_("No Items available for transfer")) if rm_items_list: @@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items): 'qty': rm_item_data["qty"], 'from_warehouse': rm_item_data["warehouse"], 'stock_uom': rm_item_data["stock_uom"], + 'serial_no': rm_item_data.get('serial_no'), + 'batch_no': rm_item_data.get('batch_no'), 'main_item_code': rm_item_data["item_code"], 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') } @@ -582,3 +585,57 @@ def update_status(status, name): def make_inter_company_sales_order(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction return make_inter_company_transaction("Purchase Order", source_name, target_doc) + +@frappe.whitelist() +def get_materials_from_supplier(purchase_order, po_details): + if isinstance(po_details, string_types): + po_details = json.loads(po_details) + + doc = frappe.get_cached_doc('Purchase Order', purchase_order) + doc.initialized_fields() + doc.purchase_orders = [doc.name] + doc.get_available_materials() + + if not doc.available_materials: + frappe.throw(_('Materials are already received against the purchase order {0}') + .format(purchase_order)) + + return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details) + +def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details): + ste_doc = frappe.new_doc('Stock Entry') + ste_doc.purpose = 'Material Transfer' + ste_doc.purchase_order = po_doc.name + ste_doc.company = po_doc.company + ste_doc.is_return = 1 + + for key, value in available_materials.items(): + if not value.qty: + continue + + if value.batch_no: + for batch_no, qty in value.batch_no.items(): + add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) + else: + add_items_in_ste(ste_doc, value, value.qty, po_details) + + ste_doc.set_stock_entry_type() + ste_doc.calculate_rate_and_amount() + + return ste_doc + +def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): + item = ste_doc.append('items', row.item_details) + + po_detail = list(set(row.po_details).intersection(po_details)) + item.update({ + 'qty': qty, + 'batch_no': batch_no, + 'basic_rate': row.item_details['rate'], + 'po_detail': po_detail[0] if po_detail else '', + 's_warehouse': row.item_details['t_warehouse'], + 't_warehouse': row.item_details['s_warehouse'], + 'item_code': row.item_details['rm_item_code'], + 'subcontracted_item': row.item_details['main_item_code'], + 'serial_no': '\n'.join(row.serial_no) if row.serial_no else '' + }) \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 3b9f8e9775..33d1971451 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.batch.test_batch import make_new_batch -from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) def test_exploded_items_in_subcontracted(self): - item_code = "_Test Subcontracted FG Item 1" + item_code = "_Test Subcontracted FG Item 11" make_subcontracted_item(item_code=item_code) po = create_purchase_order(item_code=item_code, qty=1, @@ -853,76 +852,6 @@ class TestPurchaseOrder(unittest.TestCase): update_backflush_based_on("BOM") - def test_backflushed_based_on_for_multiple_batches(self): - item_code = "_Test Subcontracted FG Item 2" - make_item('Sub Contracted Raw Material 2', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1 - }) - - make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1, - raw_materials=["Sub Contracted Raw Material 2"]) - - update_backflush_based_on("Material Transferred for Subcontract") - - order_qty = 500 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") - - make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100) - - rm_items = [ - {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item", - "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}] - - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.submit() - - for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]: - make_new_batch(batch_id=batch, item_code=item_code) - - pr = make_purchase_receipt(po.name) - - # partial receipt - pr.get('items')[0].qty = 30 - pr.get('items')[0].batch_no = "ABCD1" - - purchase_order = po.name - purchase_order_item = po.items[0].name - - for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items(): - pr.append("items", { - "item_code": pr.get('items')[0].item_code, - "item_name": pr.get('items')[0].item_name, - "uom": pr.get('items')[0].uom, - "stock_uom": pr.get('items')[0].stock_uom, - "warehouse": pr.get('items')[0].warehouse, - "conversion_factor": pr.get('items')[0].conversion_factor, - "cost_center": pr.get('items')[0].cost_center, - "rate": pr.get('items')[0].rate, - "qty": qty, - "batch_no": batch_no, - "purchase_order": purchase_order, - "purchase_order_item": purchase_order_item - }) - - pr.submit() - - pr1 = make_purchase_receipt(po.name) - pr1.get('items')[0].qty = 300 - pr1.get('items')[0].batch_no = "ABCD1" - pr1.save() - - pr_key = ("Sub Contracted Raw Material 2", po.name) - consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key) - - self.assertTrue(pr1.supplied_items[0].consumed_qty > 0) - self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty)) - - update_backflush_based_on("BOM") - def test_supplied_qty_against_subcontracted_po(self): item_code = "_Test Subcontracted FG Item 5" make_item('Sub Contracted Raw Material 4', { @@ -1117,22 +1046,29 @@ def create_purchase_order(**args): po.conversion_factor = args.conversion_factor or 1 po.supplier_warehouse = args.supplier_warehouse or None - po.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 10, - "rate": args.rate or 500, - "schedule_date": add_days(nowdate(), 1), - "include_exploded_items": args.get('include_exploded_items', 1), - "against_blanket_order": args.against_blanket_order - }) + if args.rm_items: + for row in args.rm_items: + po.append("items", row) + else: + po.append("items", { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 10, + "rate": args.rate or 500, + "schedule_date": add_days(nowdate(), 1), + "include_exploded_items": args.get('include_exploded_items', 1), + "against_blanket_order": args.against_blanket_order + }) + + po.set_missing_values() if not args.do_not_save: po.insert() if not args.do_not_submit: if po.is_subcontracted == "Yes": supp_items = po.get("supplied_items") for d in supp_items: - d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" + if not d.reserve_warehouse: + d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" po.submit() return po diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index d7ea9c1ccc..505ecd84c5 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -6,21 +6,25 @@ "engine": "InnoDB", "field_order": [ "main_item_code", - "bom_detail_no", + "rm_item_code", + "column_break_3", "stock_uom", + "reserve_warehouse", "conversion_factor", "column_break_6", - "rm_item_code", + "bom_detail_no", "reference_name", - "reserve_warehouse", "section_break2", "rate", "col_break2", "amount", "section_break1", "required_qty", + "supplied_qty", "col_break1", - "supplied_qty" + "returned_qty", + "total_supplied_qty", + "consumed_qty" ], "fields": [ { @@ -125,6 +129,8 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Supplied Qty", + "no_copy": 1, + "print_hide": 1, "read_only": 1 }, { @@ -142,13 +148,42 @@ { "fieldname": "col_break2", "fieldtype": "Column Break" + }, + { + "fieldname": "consumed_qty", + "fieldtype": "Float", + "label": "Consumed Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "total_supplied_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Total Supplied Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-06-01 00:41:54.123436", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index dc00bca5cc..d8c37f5881 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -6,10 +6,11 @@ "engine": "InnoDB", "field_order": [ "main_item_code", - "description", + "rm_item_code", + "item_name", "bom_detail_no", "col_break1", - "rm_item_code", + "description", "stock_uom", "conversion_factor", "reference_name", @@ -52,7 +53,6 @@ "fieldname": "description", "fieldtype": "Text Editor", "in_global_search": 1, - "in_list_view": 1, "label": "Description", "oldfieldname": "description", "oldfieldtype": "Data", @@ -87,12 +87,13 @@ "read_only": 1 }, { + "columns": 2, "fieldname": "consumed_qty", "fieldtype": "Float", + "in_list_view": 1, "label": "Consumed Qty", "oldfieldname": "consumed_qty", "oldfieldtype": "Currency", - "read_only": 1, "reqd": 1 }, { @@ -183,12 +184,18 @@ { "fieldname": "col_break4", "fieldtype": "Column Break" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-05-29 17:22:14.977117", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", diff --git a/erpnext/buying/report/subcontract_order_summary/__init__.py b/erpnext/buying/report/subcontract_order_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js new file mode 100644 index 0000000000..5ba52f1b21 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -0,0 +1,45 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Subcontract Order Summary"] = { + "filters": [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1 + }, + { + label: __("Purchase Order"), + fieldname: "name", + fieldtype: "Link", + options: "Purchase Order", + get_query: function() { + return { + filters: { + docstatus: 1, + is_subcontracted: 'Yes', + company: frappe.query_report.get_filter_value('company') + } + } + } + } + ] +}; diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json new file mode 100644 index 0000000000..526a8d8ad0 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-05-31 14:43:32.417694", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-05-31 14:43:32.417694", + "modified_by": "Administrator", + "module": "Buying", + "name": "Subcontract Order Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Subcontract Order Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + } + ] +} \ No newline at end of file diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py new file mode 100644 index 0000000000..8b08d2a284 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -0,0 +1,158 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns, data = [], [] + columns = get_columns() + data = get_data(filters) + + return columns, data + +def get_data(report_filters): + data = [] + orders = get_subcontracted_orders(report_filters) + + if orders: + supplied_items = get_supplied_items(orders, report_filters) + po_details = prepare_subcontracted_data(orders, supplied_items) + get_subcontracted_data(po_details, data) + + return data + +def get_subcontracted_orders(report_filters): + fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`', + '`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`', + '`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`'] + + filters = get_filters(report_filters) + + return frappe.get_all('Purchase Order', fields = fields, filters=filters) or [] + +def get_filters(report_filters): + filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'], + ['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]] + + for field in ['name', 'company']: + if report_filters.get(field): + filters.append(['Purchase Order', field, '=', report_filters.get(field)]) + + return filters + +def get_supplied_items(orders, report_filters): + if not orders: + return [] + + fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty', + 'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name'] + + filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1} + + supplied_items = {} + for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters): + new_key = (row.parent, row.reference_name, row.main_item_code) + + supplied_items.setdefault(new_key, []).append(row) + + return supplied_items + +def prepare_subcontracted_data(orders, supplied_items): + po_details = {} + for row in orders: + key = (row.po_id, row.name, row.item_code) + if key not in po_details: + po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []})) + + details = po_details[key] + + if supplied_items.get(key): + for supplied_item in supplied_items[key]: + details['supplied_items'].append(supplied_item) + + return po_details + +def get_subcontracted_data(po_details, data): + for key, details in po_details.items(): + res = details.po_item + for index, row in enumerate(details.supplied_items): + if index != 0: + res = {} + + res.update(row) + data.append(res) + +def get_columns(): + return [ + { + "label": _("Id"), + "fieldname": "po_id", + "fieldtype": "Link", + "options": "Purchase Order", + "width": 100 + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "width": 80 + }, + { + "label": _("Subcontracted Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140 + }, + { + "label": _("Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 70 + }, + { + "label": _("Received"), + "fieldname": "received_qty", + "fieldtype": "Float", + "width": 80 + }, + { + "label": _("Supplied Item"), + "fieldname": "rm_item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140 + }, + { + "label": _("Required Qty"), + "fieldname": "required_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Supplied Qty"), + "fieldname": "supplied_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Returned Qty"), + "fieldname": "returned_qty", + "fieldtype": "Float", + "width": 110 + }, + { + "label": _("Total Supplied"), + "fieldname": "total_supplied_qty", + "fieldtype": "Float", + "width": 120 + }, + { + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", + "fieldtype": "Float", + "width": 110 + }, + ] \ No newline at end of file diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 20f5445725..1907885717 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details from erpnext.stock.get_item_details import get_conversion_factor from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.stock.stock_ledger import get_valuation_rate -from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos from frappe.contacts.doctype.address.address import get_address_display from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget -from erpnext.controllers.stock_controller import StockController from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.stock.utils import get_incoming_rate -class BuyingController(StockController): +from erpnext.controllers.stock_controller import StockController +from erpnext.controllers.subcontracting import Subcontracting + +class BuyingController(StockController, Subcontracting): def get_feed(self): if self.get("supplier_name"): @@ -256,7 +257,7 @@ class BuyingController(StockController): supplied_items_cost = 0.0 for d in self.get("supplied_items"): if d.reference_name == item_row_id: - if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'): + if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'): rate = get_incoming_rate({ "item_code": d.rm_item_code, "warehouse": self.supplier_warehouse, @@ -298,23 +299,7 @@ class BuyingController(StockController): def create_raw_materials_supplied(self, raw_material_table): if self.is_subcontracted=="Yes": - parent_items = [] - backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings", - "backflush_raw_materials_of_subcontract_based_on") - if (self.doctype == 'Purchase Receipt' and - backflush_raw_materials_based_on != 'BOM'): - self.update_raw_materials_supplied_based_on_stock_entries() - else: - for item in self.get("items"): - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - item.rm_supp_cost = 0.0 - if item.bom and item.item_code in self.sub_contracted_items: - self.update_raw_materials_supplied_based_on_bom(item, raw_material_table) - - if [item.item_code, item.name] not in parent_items: - parent_items.append([item.item_code, item.name]) - - self.cleanup_raw_materials_supplied(parent_items, raw_material_table) + self.set_materials_for_subcontracted_items(raw_material_table) elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: for item in self.get("items"): @@ -323,176 +308,6 @@ class BuyingController(StockController): if self.is_subcontracted == "No" and self.get("supplied_items"): self.set('supplied_items', []) - def update_raw_materials_supplied_based_on_stock_entries(self): - self.set('supplied_items', []) - - purchase_orders = set(d.purchase_order for d in self.items) - - # qty of raw materials backflushed (for each item per purchase order) - backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders) - - # qty of "finished good" item yet to be received - qty_to_be_received_map = get_qty_to_be_received(purchase_orders) - - for item in self.get('items'): - if not item.purchase_order: - continue - - # reset raw_material cost - item.rm_supp_cost = 0 - - # qty of raw materials transferred to the supplier - transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code) - - non_stock_items = get_non_stock_items(item.purchase_order, item.item_code) - - item_key = '{}{}'.format(item.item_code, item.purchase_order) - - fg_yet_to_be_received = qty_to_be_received_map.get(item_key) - - if not fg_yet_to_be_received: - frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}") - .format(item.idx, frappe.bold(item.item_code), - frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)), - title=_("Limit Crossed")) - - transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) - # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) - - for raw_material in transferred_raw_materials + non_stock_items: - rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order) - raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) - - consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_no', '') - consumed_batch_nos = raw_material_data.get('batch_nos', '') - - transferred_qty = raw_material.qty - - rm_qty_to_be_consumed = transferred_qty - consumed_qty - - # backflush all remaining transferred qty in the last Purchase Receipt - if fg_yet_to_be_received == item.qty: - qty = rm_qty_to_be_consumed - else: - qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty - - if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'): - qty = frappe.utils.ceil(qty) - - if qty > rm_qty_to_be_consumed: - qty = rm_qty_to_be_consumed - - if not qty: continue - - if raw_material.serial_nos: - set_serial_nos(raw_material, consumed_serial_nos, qty) - - if raw_material.batch_nos: - backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {}) - - batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, - qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) - - for batch_data in batches_qty: - qty = batch_data['qty'] - raw_material.batch_no = batch_data['batch'] - if qty > 0: - self.append_raw_material_to_be_backflushed(item, raw_material, qty) - else: - self.append_raw_material_to_be_backflushed(item, raw_material, qty) - - def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty): - rm = self.append('supplied_items', {}) - rm.update(raw_material_data) - - if not rm.main_item_code: - rm.main_item_code = fg_item_row.item_code - - rm.reference_name = fg_item_row.name - rm.required_qty = qty - rm.consumed_qty = qty - - def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): - exploded_item = 1 - if hasattr(item, 'include_exploded_items'): - exploded_item = item.get('include_exploded_items') - - bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item) - - used_alternative_items = [] - if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order: - used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order) - - raw_materials_cost = 0 - items = list(set([d.item_code for d in bom_items])) - item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse - from `tabItem` i, `tabItem Default` id - where id.parent=i.name and id.company=%s and i.name in ({0})""" - .format(", ".join(["%s"] * len(items))), [self.company] + items)) - - for bom_item in bom_items: - if self.doctype == "Purchase Order": - reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code) - if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company: - reserve_warehouse = None - - conversion_factor = item.conversion_factor - if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and - bom_item.item_code in used_alternative_items): - alternative_item_data = used_alternative_items.get(bom_item.item_code) - bom_item.item_code = alternative_item_data.item_code - bom_item.item_name = alternative_item_data.item_name - bom_item.stock_uom = alternative_item_data.stock_uom - conversion_factor = alternative_item_data.conversion_factor - bom_item.description = alternative_item_data.description - - # check if exists - exists = 0 - for d in self.get(raw_material_table): - if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \ - and d.reference_name == item.name: - rm, exists = d, 1 - break - - if not exists: - rm = self.append(raw_material_table, {}) - - required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) * - flt(conversion_factor), rm.precision("required_qty")) - rm.reference_name = item.name - rm.bom_detail_no = bom_item.name - rm.main_item_code = item.item_code - rm.rm_item_code = bom_item.item_code - rm.stock_uom = bom_item.stock_uom - rm.required_qty = required_qty - rm.rate = bom_item.rate - rm.conversion_factor = conversion_factor - - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - rm.consumed_qty = required_qty - rm.description = bom_item.description - if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no: - rm.batch_no = item.batch_no - elif not rm.reserve_warehouse: - rm.reserve_warehouse = reserve_warehouse - - def cleanup_raw_materials_supplied(self, parent_items, raw_material_table): - """Remove all those child items which are no longer present in main item table""" - delete_list = [] - for d in self.get(raw_material_table): - if [d.main_item_code, d.reference_name] not in parent_items: - # mark for deletion from doclist - delete_list.append(d) - - # delete from doclist - if delete_list: - rm_supplied_details = self.get(raw_material_table) - self.set(raw_material_table, []) - for d in rm_supplied_details: - if d not in delete_list: - self.append(raw_material_table, d) - @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): @@ -867,104 +682,6 @@ class BuyingController(StockController): else: validate_item_type(self, "is_purchase_item", "purchase") - -def get_items_from_bom(item_code, bom, exploded_item=1): - doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" - - bom_items = frappe.db.sql("""select t2.item_code, t2.name, - t2.rate, t2.stock_uom, t2.source_warehouse, t2.description, - t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit - from - `tabBOM` t1, `tab{0}` t2, tabItem t3 - where - t2.parent = t1.name and t1.item = %s - and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s - and t2.sourced_by_supplier = 0 - and t2.item_code = t3.name""".format(doctype), - (item_code, bom), as_dict=1) - - if not bom_items: - msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1) - - return bom_items - -def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): - common_query = """ - SELECT - sed.item_code AS rm_item_code, - SUM(sed.qty) AS qty, - sed.description, - sed.stock_uom, - sed.subcontracted_item AS main_item_code, - {serial_no_concat_syntax} AS serial_nos, - {batch_no_concat_syntax} AS batch_nos - FROM `tabStock Entry` se,`tabStock Entry Detail` sed - WHERE - se.name = sed.parent - AND se.docstatus=1 - AND se.purpose='Send to Subcontractor' - AND se.purchase_order = %s - AND IFNULL(sed.t_warehouse, '') != '' - AND IFNULL(sed.subcontracted_item, '') in ('', %s) - GROUP BY sed.item_code, sed.subcontracted_item - """ - raw_materials = frappe.db.multisql({ - 'mariadb': common_query.format( - serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)", - batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)" - ), - 'postgres': common_query.format( - serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')", - batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')" - ) - }, (purchase_order, fg_item), as_dict=1) - - return raw_materials - -def get_backflushed_subcontracted_raw_materials(purchase_orders): - purchase_receipts = frappe.get_all("Purchase Receipt Item", - fields = ["purchase_order", "item_code", "name", "parent"], - filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))}) - - distinct_purchase_receipts = {} - for pr in purchase_receipts: - key = (pr.purchase_order, pr.item_code, pr.parent) - distinct_purchase_receipts.setdefault(key, []).append(pr.name) - - backflushed_raw_materials_map = frappe._dict() - for args, references in iteritems(distinct_purchase_receipts): - purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references) - - for data in purchase_receipt_supplied_items: - pr_key = (data.rm_item_code, data.main_item_code, args[0]) - if pr_key not in backflushed_raw_materials_map: - backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ - "qty": 0.0, - "serial_no": [], - "batch_no": [], - "consumed_batch": {} - })) - - row = backflushed_raw_materials_map.get(pr_key) - row.qty += data.consumed_qty - - for field in ["serial_no", "batch_no"]: - if data.get(field): - row[field].append(data.get(field)) - - if data.get("batch_no"): - if data.get("batch_no") in row.consumed_batch: - row.consumed_batch[data.get("batch_no")] += data.consumed_qty - else: - row.consumed_batch[data.get("batch_no")] = data.consumed_qty - - return backflushed_raw_materials_map - -def get_supplied_items(item_code, purchase_receipt, references): - return frappe.get_all("Purchase Receipt Item Supplied", - fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"], - filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)}) - def get_asset_item_details(asset_items): asset_items_data = {} for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], @@ -996,135 +713,3 @@ def validate_item_type(doc, fieldname, message): error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) frappe.throw(error_message) - -def get_qty_to_be_received(purchase_orders): - return frappe._dict(frappe.db.sql(""" - SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key, - SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received - FROM `tabPurchase Order Item` poi - WHERE - poi.`parent` in %s - GROUP BY poi.`item_code`, poi.`parent` - HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`) - """, (purchase_orders))) - -def get_non_stock_items(purchase_order, fg_item_code): - return frappe.db.sql(""" - SELECT - pois.main_item_code, - pois.rm_item_code, - item.description, - pois.required_qty AS qty, - pois.rate, - 1 as non_stock_item, - pois.stock_uom - FROM `tabPurchase Order Item Supplied` pois, `tabItem` item - WHERE - pois.`rm_item_code` = item.`name` - AND item.is_stock_item = 0 - AND pois.`parent` = %s - AND pois.`main_item_code` = %s - """, (purchase_order, fg_item_code), as_dict=1) - - -def set_serial_nos(raw_material, consumed_serial_nos, qty): - serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \ - set(get_serial_nos(consumed_serial_nos)) - if serial_nos and qty <= len(serial_nos): - raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)]) - -def get_transferred_batch_qty_map(purchase_order, fg_item): - # returns - # { - # (item_code, fg_code): { - # batch1: 10, # qty - # batch2: 16 - # }, - # } - transferred_batch_qty_map = {} - transferred_batches = frappe.db.sql(""" - SELECT - sed.batch_no, - SUM(sed.qty) AS qty, - sed.item_code, - sed.subcontracted_item - FROM `tabStock Entry` se,`tabStock Entry Detail` sed - WHERE - se.name = sed.parent - AND se.docstatus=1 - AND se.purpose='Send to Subcontractor' - AND se.purchase_order = %s - AND ifnull(sed.subcontracted_item, '') in ('', %s) - AND sed.batch_no IS NOT NULL - GROUP BY - sed.batch_no, - sed.item_code - """, (purchase_order, fg_item), as_dict=1) - - for batch_data in transferred_batches: - key = ((batch_data.item_code, fg_item) - if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) - transferred_batch_qty_map.setdefault(key, OrderedDict()) - transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty - - return transferred_batch_qty_map - -def get_backflushed_batch_qty_map(purchase_order, fg_item): - # returns - # { - # (item_code, fg_code): { - # batch1: 10, # qty - # batch2: 16 - # }, - # } - backflushed_batch_qty_map = {} - backflushed_batches = frappe.db.sql(""" - SELECT - pris.batch_no, - SUM(pris.consumed_qty) AS qty, - pris.rm_item_code AS item_code - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris - WHERE - pr.name = pri.parent - AND pri.parent = pris.parent - AND pri.purchase_order = %s - AND pri.item_code = pris.main_item_code - AND pr.docstatus = 1 - AND pris.main_item_code = %s - AND pris.batch_no IS NOT NULL - GROUP BY - pris.rm_item_code, pris.batch_no - """, (purchase_order, fg_item), as_dict=1) - - for batch_data in backflushed_batches: - backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) - backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty - - return backflushed_batch_qty_map - -def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po): - # Returns available batches to be backflushed based on requirements - transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) - if not transferred_batches: - transferred_batches = transferred_batch_qty_map.get((item_code, po), {}) - - available_batches = [] - - for (batch, transferred_qty) in transferred_batches.items(): - backflushed_qty = backflushed_batches.get(batch, 0) - available_qty = transferred_qty - backflushed_qty - - if available_qty >= required_qty: - available_batches.append({'batch': batch, 'qty': required_qty}) - break - elif available_qty != 0: - available_batches.append({'batch': batch, 'qty': available_qty}) - required_qty -= available_qty - - for row in available_batches: - if backflushed_batches.get(row.get('batch'), 0) > 0: - backflushed_batches[row.get('batch')] += row.get('qty') - else: - backflushed_batches[row.get('batch')] = row.get('qty') - - return available_batches diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py new file mode 100644 index 0000000000..fe775766da --- /dev/null +++ b/erpnext/controllers/subcontracting.py @@ -0,0 +1,342 @@ +from __future__ import unicode_literals + +import frappe +from frappe import _ +from frappe.utils import flt, cint +from collections import defaultdict +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + +class Subcontracting(object): + def set_materials_for_subcontracted_items(self, raw_material_table): + if self.doctype == 'Purchase Invoice' and not self.update_stock: + return + + self.raw_material_table = raw_material_table + self.identify_change_in_item_table() + self.prepare_supplied_items() + self.validate_consumed_qty() + + def prepare_supplied_items(self): + self.initialized_fields() + self.get_purchase_orders() + self.get_pending_qty_to_receive() + self.get_available_materials() + self.remove_changed_rows() + self.set_supplied_items() + + def initialized_fields(self): + self.available_materials = frappe._dict() + self.alternative_item_details = frappe._dict() + self.get_backflush_based_on() + + def get_backflush_based_on(self): + self.backflush_based_on = frappe.db.get_single_value("Buying Settings", + "backflush_raw_materials_of_subcontract_based_on") + + def get_purchase_orders(self): + self.purchase_orders = [] + + if self.doctype == 'Purchase Order': + return + + self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] + + def identify_change_in_item_table(self): + self.changed_name = [] + + if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table): + self.set(self.raw_material_table, []) + return + + item_dict = self.get_data_before_save() + if not item_dict: + return True + + for n_row in self.items: + if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]: + self.changed_name.append(n_row.name) + + if item_dict.get(n_row.name): + del item_dict[n_row.name] + + self.changed_name.extend(item_dict.keys()) + + def get_data_before_save(self): + item_dict = {} + if self.doctype == 'Purchase Receipt' and self._doc_before_save: + for row in self._doc_before_save.get('items'): + item_dict[row.name] = (row.item_code, row.qty) + + return item_dict + + def get_available_materials(self): + ''' Get the available raw materials which has been transferred to the supplier. + available_materials = { + (item_code, subcontracted_item, purchase_order): { + 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details + } + } + ''' + if not self.purchase_orders: + return + + for row in self.get_transferred_items(): + key = (row.rm_item_code, row.main_item_code, row.purchase_order) + + if key not in self.available_materials: + self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [], + 'batch_no': defaultdict(float), 'item_details': row, 'po_details': []}) + ) + + details = self.available_materials[key] + details.qty += row.qty + details.po_details.append(row.po_detail) + + if row.serial_no: + details.serial_no.extend(get_serial_nos(row.serial_no)) + + if row.batch_no: + details.batch_no[row.batch_no] += row.qty + + self.set_alternative_item_details(row) + + for doctype in ['Purchase Receipt', 'Purchase Invoice']: + self.remove_consumed_materials(doctype) + + def remove_consumed_materials(self, doctype, return_consumed_items=False): + '''Deduct the consumed materials from the available materials.''' + + pr_items = self.get_received_items(doctype) + if not pr_items: + return ([], {}) if return_consumed_items else None + + pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items} + consumed_materials = self.get_consumed_items(doctype, pr_items.keys()) + + if return_consumed_items: + return (consumed_materials, pr_items) + + for row in consumed_materials: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + if not self.available_materials.get(key): + continue + + self.available_materials[key]['qty'] -= row.consumed_qty + if row.serial_no: + self.available_materials[key]['serial_no'] = list( + set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no)) + ) + + if row.batch_no: + self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty + + def get_transferred_items(self): + fields = ['`tabStock Entry`.`purchase_order`'] + alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'} + + child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount', + 'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor', + 's_warehouse', 't_warehouse', 'item_group', 'po_detail'] + + if self.backflush_based_on == 'BOM': + child_table_fields.append('original_item') + + for field in child_table_fields: + fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}') + + filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'], + ['Stock Entry', 'purchase_order', 'in', self.purchase_orders]] + + return frappe.get_all('Stock Entry', fields = fields, filters=filters) + + def get_received_items(self, doctype): + fields = [] + self.po_field = 'purchase_order' if doctype == 'Purchase Receipt' else 'po_detail' + + for field in ['name', self.po_field, 'parent']: + fields.append(f'`tab{doctype} Item`.`{field}`') + + filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]] + if doctype == 'Purchase Invoice': + filters.append(['Purchase Invoice', 'update_stock', "=", 1]) + + return frappe.get_all(f'{doctype}', fields = fields, filters = filters) + + def get_consumed_items(self, doctype, pr_items): + return frappe.get_all(f'{doctype} Item Supplied', + fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], + filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items))}) + + def set_alternative_item_details(self, row): + if row.get('original_item'): + self.alternative_item_details[row.get('original_item')] = row + + def get_pending_qty_to_receive(self): + '''Get qty to be received against the purchase order.''' + + self.qty_to_be_received = defaultdict(float) + + if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders: + for row in frappe.get_all('Purchase Order Item', + fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'], + filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}): + + self.qty_to_be_received[(row.item_code, row.parent)] += row.qty + + def get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item' + fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit'] + + alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'} + for field in ['item_code', 'name', 'rate', 'stock_uom', + 'source_warehouse', 'description', 'item_name', 'stock_uom']: + fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}') + + filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1], + ['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]] + + return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] + + def remove_changed_rows(self): + if not self.changed_name: + return + + i=1 + self.set(self.raw_material_table, []) + for d in self._doc_before_save.supplied_items: + if d.reference_name in self.changed_name: + continue + + d.idx = i + self.append('supplied_items', d) + + i += 1 + + def set_supplied_items(self): + self.bom_items = {} + + has_supplied_items = True if self.get(self.raw_material_table) else False + for row in self.items: + if (self.doctype != 'Purchase Order' and ((self.changed_name and row.name not in self.changed_name) + or (has_supplied_items and not self.changed_name))): + continue + + if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': + for bom_item in self.get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): + qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor) + bom_item.main_item_code = row.item_code + self.update_reserve_warehouse(bom_item, row) + self.set_alternative_item(bom_item) + self.add_supplied_item(row, bom_item, qty) + + elif self.backflush_based_on != 'BOM': + for key, transfer_item in self.available_materials.items(): + if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0: + qty = self.get_qty_based_on_material_transfer(row, transfer_item) or 0 + transfer_item.qty -= qty + self.add_supplied_item(row, transfer_item.get('item_details'), qty) + + if self.qty_to_be_received: + self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty + + def update_reserve_warehouse(self, row, item): + if self.doctype == 'Purchase Order': + row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse) + + def get_qty_based_on_material_transfer(self, item_row, transfer_item): + key = (item_row.item_code, item_row.purchase_order) + + if self.qty_to_be_received == item_row.qty: + return transfer_item.qty + + if self.qty_to_be_received: + qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + if (transfer_item.serial_no or frappe.get_cached_value('UOM', + transfer_item.item_details.stock_uom, 'must_be_whole_number')): + return frappe.utils.ceil(qty) + + return qty + + def set_alternative_item(self, bom_item): + if self.alternative_item_details.get(bom_item.rm_item_code): + bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) + + def add_supplied_item(self, item_row, bom_item, qty): + bom_item.conversion_factor = item_row.conversion_factor + rm_obj = self.append(self.raw_material_table, bom_item) + rm_obj.reference_name = item_row.name + + if self.doctype == 'Purchase Order': + rm_obj.required_qty = qty + else: + self.set_batch_nos(bom_item, item_row, rm_obj, qty) + + def set_batch_nos(self, bom_item, item_row, rm_obj, qty): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) + + if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): + for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): + if batch_qty >= qty: + self.set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) + self.available_materials[key]['batch_no'][batch_no] -= qty + return + + elif qty > 0 and batch_qty > 0: + qty -= batch_qty + new_rm_obj = self.append(self.raw_material_table, bom_item) + new_rm_obj.reference_name = item_row.name + self.set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) + self.available_materials[key]['batch_no'][batch_no] = 0 + else: + rm_obj.required_qty = qty + rm_obj.consumed_qty = qty + self.set_serial_nos(item_row, rm_obj) + + def set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): + rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, 'required_qty': qty}) + self.set_serial_nos(item_row, rm_obj) + + def set_serial_nos(self, item_row, rm_obj): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) + if (self.available_materials.get(key) and self.available_materials[key]['serial_no']): + used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)] + rm_obj.serial_no = '\n'.join(used_serial_nos) + + # Removed the used serial nos from the list + for sn in used_serial_nos: + self.available_materials[key]['serial_no'].remove(sn) + + def set_consumed_qty_in_po(self): + if self.is_subcontracted != 'Yes': + return + + self.get_purchase_orders() + consumed_items, pr_items = self.remove_consumed_materials(self.doctype, return_consumed_items=True) + + itemwise_consumed_qty = defaultdict(float) + for row in consumed_items: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + itemwise_consumed_qty[key] += row.consumed_qty + + self.update_consumed_qty_in_po(itemwise_consumed_qty) + + def update_consumed_qty_in_po(self, itemwise_consumed_qty): + fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name'] + filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)} + + for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'): + key = (row.rm_item_code, row.main_item_code, row.parent) + consumed_qty = itemwise_consumed_qty.get(key, 0) + + if row.supplied_qty < consumed_qty: + consumed_qty = row.supplied_qty + + itemwise_consumed_qty[key] -= consumed_qty + frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) + + def validate_consumed_qty(self): + for row in self.get(self.raw_material_table): + if flt(row.consumed_qty) == 0.0 and row.get('serial_no'): + msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + + frappe.throw(_(msg),title=_('Consumed Items Qty Check')) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index e1cca9e3ef..42b23f223d 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update from six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.tests.test_subcontracting import set_backflush_based_on test_records = frappe.get_test_records('BOM') @@ -160,6 +161,7 @@ class TestBOM(unittest.TestCase): def test_subcontractor_sourced_item(self): item_code = "_Test Subcontracted FG Item 1" + set_backflush_based_on('Material Transferred for Subcontract') if not frappe.db.exists('Item', item_code): make_item(item_code, { diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 0514bd2394..43642013ce 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -54,7 +54,7 @@ class Bin(Document): self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty")) self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty")) self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty")) - + self.set_projected_qty() self.db_update() @@ -115,7 +115,7 @@ class Bin(Document): #Get Transferred Entries materials_transferred = frappe.db.sql(""" select - ifnull(sum(transfer_qty),0) + ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0) from `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po where diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index d5700fe514..8f76844bde 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -18,6 +18,9 @@ class TestItemAlternative(unittest.TestCase): make_items() def test_alternative_item_for_subcontract_rm(self): + frappe.db.set_value('Buying Settings', None, + 'backflush_raw_materials_of_subcontract_based_on', 'BOM') + create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC', qty=5, rate=2000) create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', @@ -65,6 +68,8 @@ class TestItemAlternative(unittest.TestCase): status = True self.assertEqual(status, True) + frappe.db.set_value('Buying Settings', None, + 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract') def test_alternative_item_for_production_rm(self): create_stock_reconciliation(item_code='Alternate Item For A RW 1', diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index befdad9692..887b15a211 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -41,6 +41,8 @@ frappe.ui.form.on("Purchase Receipt", { } }); + frm.set_df_property('supplied_items', 'cannot_add_rows', 1); + }, onload: function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index ad350d344f..44fb736304 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -514,8 +514,7 @@ "oldfieldname": "pr_raw_material_details", "oldfieldtype": "Table", "options": "Purchase Receipt Item Supplied", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "section_break0", @@ -1149,7 +1148,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2021-04-19 01:01:00.754119", + "modified": "2021-05-25 00:15:12.239017", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 83ba324495..b8580f95a3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -202,6 +202,7 @@ class PurchaseReceipt(BuyingController): self.make_gl_entries() self.repost_future_sle_and_gle() + self.set_consumed_qty_in_po() def check_next_docstatus(self): submit_rv = frappe.db.sql("""select t1.name @@ -233,6 +234,7 @@ class PurchaseReceipt(BuyingController): self.repost_future_sle_and_gle() self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.delete_auto_created_batches() + self.set_consumed_qty_in_po() @frappe.whitelist() def get_current_stock(self): diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8d9b675bed..95096d77d7 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -335,6 +335,10 @@ class TestPurchaseReceipt(unittest.TestCase): se2.cancel() se3.cancel() po.reload() + pr2.load_from_db() + pr2.cancel() + + po.load_from_db() po.cancel() def test_serial_no_supplier(self): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 1a25994b24..6708393027 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1079,6 +1079,10 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { } function attach_bom_items(bom_no) { + if (!bom_no) { + return + } + if (check_should_not_attach_bom_items(bom_no)) return frappe.db.get_doc("BOM",bom_no).then(bom => { const {name, items} = bom diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index a0b5457dd7..523d332b8f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -74,7 +74,8 @@ "total_amount", "job_card", "amended_from", - "credit_note" + "credit_note", + "is_return" ], "fields": [ { @@ -611,6 +612,16 @@ "fieldname": "apply_putaway_rule", "fieldtype": "Check", "label": "Apply Putaway Rule" + }, + { + "default": "0", + "fieldname": "is_return", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Return", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -618,7 +629,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-24 11:32:23.904307", + "modified": "2021-05-26 17:07:58.015737", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 560ceaa917..213280870a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -97,8 +97,7 @@ class StockEntry(StockController): update_serial_nos_after_submit(self, "items") self.update_work_order() self.validate_purchase_order() - if self.purchase_order and self.purpose == "Send to Subcontractor": - self.update_purchase_order_supplied_items() + self.update_purchase_order_supplied_items() self.make_gl_entries() @@ -117,9 +116,7 @@ class StockEntry(StockController): self.set_material_request_transfer_status('Completed') def on_cancel(self): - - if self.purchase_order and self.purpose == "Send to Subcontractor": - self.update_purchase_order_supplied_items() + self.update_purchase_order_supplied_items() if self.work_order and self.purpose == "Material Consumption for Manufacture": self.validate_work_order_status() @@ -1347,7 +1344,7 @@ class StockEntry(StockController): se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) for field in ["idx", "po_detail", "original_item", - "expense_account", "description", "item_name"]: + "expense_account", "description", "item_name", "serial_no", "batch_no"]: if item_dict[d].get(field): se_child.set(field, item_dict[d].get(field)) @@ -1400,33 +1397,26 @@ class StockEntry(StockController): .format(item.batch_no, item.item_code)) def update_purchase_order_supplied_items(self): - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" - select rm_item_code, reserve_warehouse - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup - where po.name = poitemsup.parent - and po.name = %s""", self.purchase_order)) + if (self.purchase_order and + (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)): - #Update Supplied Qty in PO Supplied Items + #Get PO Supplied Items Details + item_wh = frappe._dict(frappe.db.sql(""" + select rm_item_code, reserve_warehouse + from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup + where po.name = poitemsup.parent + and po.name = %s""", self.purchase_order)) - frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos - SET - pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0) - FROM - `tabStock Entry Detail` sed, `tabStock Entry` se - WHERE - pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code - AND pos.parent = se.purchase_order AND sed.docstatus = 1 - AND se.name = sed.parent and se.purchase_order = %(po)s - ), 0) - WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order}) + supplied_items = get_supplied_items(self.purchase_order) + for name, item in supplied_items.items(): + frappe.db.set_value('Purchase Order Item Supplied', name, item) - #Update reserved sub contracted quantity in bin based on Supplied Item Details and - for d in self.get("items"): - item_code = d.get('original_item') or d.get('item_code') - reserve_warehouse = item_wh.get(item_code) - stock_bin = get_bin(item_code, reserve_warehouse) - stock_bin.update_reserved_qty_for_sub_contracting() + #Update reserved sub contracted quantity in bin based on Supplied Item Details and + for d in self.get("items"): + item_code = d.get('original_item') or d.get('item_code') + reserve_warehouse = item_wh.get(item_code) + stock_bin = get_bin(item_code, reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting() def update_so_in_serial_number(self): so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"]) @@ -1480,7 +1470,7 @@ class StockEntry(StockController): cond += """ WHEN (parent = %s and name = %s) THEN %s """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) - if cond and stock_entries_child_list: + if stock_entries_child_list: frappe.db.sql(""" UPDATE `tabStock Entry Detail` SET transferred_qty = CASE {cond} END @@ -1751,3 +1741,30 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None): format(max_retain_qty, batch_no, item_code), alert=True) sample_quantity = qty_diff return sample_quantity + +def get_supplied_items(purchase_order): + fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`', + '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`'] + + filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]] + + supplied_item_details = {} + for row in frappe.get_all('Stock Entry', fields = fields, filters = filters): + if not row.po_detail: + continue + + key = row.po_detail + if key not in supplied_item_details: + supplied_item_details.setdefault(key, + frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0})) + + supplied_item = supplied_item_details[key] + + if row.is_return: + supplied_item.returned_qty += row.transfer_qty + else: + supplied_item.supplied_qty += row.transfer_qty + + supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty) + + return supplied_item_details \ No newline at end of file diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py new file mode 100644 index 0000000000..c1a458a6dd --- /dev/null +++ b/erpnext/tests/test_subcontracting.py @@ -0,0 +1,583 @@ +from __future__ import unicode_literals +import frappe +import unittest +import copy +from frappe.utils import cint +from collections import defaultdict +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.buying.doctype.purchase_order.purchase_order import (make_rm_stock_entry, + make_purchase_receipt, get_materials_from_supplier) + +class TestSubcontracting(unittest.TestCase): + def setUp(self): + make_subcontract_items() + make_raw_materials() + make_bom_for_subcontracted_items() + + def test_po_with_bom(self): + ''' + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Create purchase receipt against the PO and check serial nos and batch no. + ''' + + set_backflush_based_on('BOM') + item_code = 'Subcontracted Item SA1' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 5, 'rate': 100}, + {'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 6, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 5}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 5}, + {'item_code': 'Subcontracted SRM Item 1', 'qty': 6}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 6}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 6} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + + for field in ['qty', 'serial_no', 'batch_no']: + if value.get(field): + transfer, consumed = (transferred_detais.get(field), value.get(field)) + if field == 'serial_no': + transfer, consumed = (sorted(transfer), sorted(consumed)) + + self.assertEqual(transfer, consumed) + + def test_po_with_material_transfer(self): + ''' + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. + - Create partial purchase receipt against the PO and check serial nos and batch no. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA1', 'qty': 5, 'rate': 100}, + {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA5', 'qty': 6, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, + {'item_code': 'Subcontracted SRM Item 5', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'}, + {'item_code': 'Subcontracted SRM Item 4', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + + make_stock_transfer_entry(po_no = po.name, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.remove(pr1.items[1]) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + + for field in ['qty', 'serial_no', 'batch_no']: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + pr2 = make_purchase_receipt(po.name) + pr2.submit() + + for key, value in get_supplied_items(pr2).items(): + transferred_detais = itemwise_details.get(key) + + for field in ['qty', 'serial_no', 'batch_no']: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + def test_subcontract_with_same_components_different_fg(self): + ''' + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of components for the item Subcontracted Item SA2. + - Create partial purchase receipt against the PO and check serial nos and batch no. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}, + {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA3', 'qty': 6, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA3'} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + + make_stock_transfer_entry(po_no = po.name, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 3 + pr1.remove(pr1.items[1]) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + self.assertEqual(value.qty, 4) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:4])) + + pr2 = make_purchase_receipt(po.name) + pr2.items[0].qty = 2 + pr2.remove(pr2.items[1]) + pr2.submit() + + for key, value in get_supplied_items(pr2).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 2) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[4:6])) + + pr3 = make_purchase_receipt(po.name) + pr3.submit() + for key, value in get_supplied_items(pr3).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 6) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[6:12])) + + def test_return_non_consumed_materials(self): + ''' + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. + - Create purchase receipt for full qty against the PO and change the qty of raw material. + - After that return the non consumed material back to the store from supplier's warehouse. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}] + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.save() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].serial_no = '\n'.join(sorted( + itemwise_details.get('Subcontracted SRM Item 2').get('serial_no')[0:5] + )) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + transferred_detais = itemwise_details.get(key) + self.assertEqual(value.qty, 5) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:5])) + + po.load_from_db() + self.assertEqual(po.supplied_items[0].consumed_qty, 5) + doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items]) + self.assertEqual(doc.items[0].qty, 1) + self.assertEqual(doc.items[0].s_warehouse, '_Test Warehouse 1 - _TC') + self.assertEqual(doc.items[0].t_warehouse, '_Test Warehouse - _TC') + self.assertEqual(get_serial_nos(doc.items[0].serial_no), + itemwise_details.get(doc.items[0].item_code)['serial_no'][5:6]) + + def test_item_with_batch_based_on_bom(self): + ''' + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + ''' + + set_backflush_based_on('BOM') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + + def test_item_with_batch_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + self.assertEqual(value.qty, qty) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 2 + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + + def test_partial_transfer_serial_no_components_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA2' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 5 + pr1.save() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3])) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + def test_partial_transfer_batch_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA6' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.items[0].qty = 5 + pr1.save() + + transferred_batch_no = '' + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + transferred_batch_no = details.batch_no + self.assertEqual(value.batch_no, details.batch_no) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0] + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + +def add_second_row_in_pr(pr): + item_dict = {} + for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom', + 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate']: + item_dict[column] = pr.items[0].get(column) + + pr.append('items', item_dict) + pr.set_missing_values() + +def get_supplied_items(pr_doc): + supplied_items = {} + for row in pr_doc.get('supplied_items'): + if row.rm_item_code not in supplied_items: + supplied_items.setdefault(row.rm_item_code, + frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + + details = supplied_items[row.rm_item_code] + update_item_details(row, details) + + return supplied_items + +def make_stock_in_entry(**args): + args = frappe._dict(args) + + items = {} + for row in args.rm_items: + row = frappe._dict(row) + + doc = make_stock_entry(target=row.warehouse or '_Test Warehouse - _TC', + item_code=row.item_code, qty=row.qty or 1, basic_rate=row.rate or 100) + + if row.item_code not in items: + items.setdefault(row.item_code, frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + + child_row = doc.items[0] + details = items[child_row.item_code] + update_item_details(child_row, details) + + return items + +def update_item_details(child_row, details): + details.qty += (child_row.get('qty') if child_row.doctype == 'Stock Entry Detail' + else child_row.get('consumed_qty')) + + if child_row.serial_no: + details.serial_no.extend(get_serial_nos(child_row.serial_no)) + + if child_row.batch_no: + details.batch_no[child_row.batch_no] += (child_row.get('qty') or child_row.get('consumed_qty')) + +def make_stock_transfer_entry(**args): + args = frappe._dict(args) + + items = [] + for row in args.rm_items: + row = frappe._dict(row) + + item = {'item_code': row.main_item_code or args.main_item_code, 'rm_item_code': row.item_code, + 'qty': row.qty or 1, 'item_name': row.item_code, 'rate': row.rate or 100, + 'stock_uom': row.stock_uom or 'Nos', 'warehouse': row.warehuose or '_Test Warehouse - _TC'} + + item_details = args.itemwise_details.get(row.item_code) + + if item_details and item_details.serial_no: + serial_nos = item_details.serial_no[0:cint(row.qty)] + item['serial_no'] = '\n'.join(serial_nos) + item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos)) + + if item_details and item_details.batch_no: + for batch_no, batch_qty in item_details.batch_no.items(): + if batch_qty >= row.qty: + item['batch_no'] = batch_no + item_details.batch_no[batch_no] -= row.qty + break + + items.append(item) + + ste_dict = make_rm_stock_entry(args.po_no, items) + doc = frappe.get_doc(ste_dict) + doc.insert() + doc.submit() + + return doc + +def make_subcontract_items(): + sub_contracted_items = {'Subcontracted Item SA1': {}, 'Subcontracted Item SA2': {}, 'Subcontracted Item SA3': {}, + 'Subcontracted Item SA4': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'SBAT.####'}, + 'Subcontracted Item SA5': {}, 'Subcontracted Item SA6': {}} + + for item, properties in sub_contracted_items.items(): + if not frappe.db.exists('Item', item): + properties.update({'is_stock_item': 1, 'is_sub_contracted_item': 1}) + make_item(item, properties) + +def make_raw_materials(): + raw_materials = {'Subcontracted SRM Item 1': {}, + 'Subcontracted SRM Item 2': {'has_serial_no': 1, 'serial_no_series': 'SRI.####'}, + 'Subcontracted SRM Item 3': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'BAT.####'}, + 'Subcontracted SRM Item 4': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}, + 'Subcontracted SRM Item 5': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}} + + for item, properties in raw_materials.items(): + if not frappe.db.exists('Item', item): + properties.update({'is_stock_item': 1}) + make_item(item, properties) + +def make_bom_for_subcontracted_items(): + boms = { + 'Subcontracted Item SA1': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], + 'Subcontracted Item SA2': ['Subcontracted SRM Item 2'], + 'Subcontracted Item SA3': ['Subcontracted SRM Item 2'], + 'Subcontracted Item SA4': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], + 'Subcontracted Item SA5': ['Subcontracted SRM Item 5'], + 'Subcontracted Item SA6': ['Subcontracted SRM Item 3'] + } + + for item_code, raw_materials in boms.items(): + if not frappe.db.exists('BOM', {'item': item_code}): + make_bom(item=item_code, raw_materials=raw_materials, rate=100) + +def set_backflush_based_on(based_on): + frappe.db.set_value('Buying Settings', None, + 'backflush_raw_materials_of_subcontract_based_on', based_on) \ No newline at end of file From 9a2db0b5b196597802ddd79119108a1b4615c3b0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 1 Jun 2021 11:24:01 +0530 Subject: [PATCH 143/550] fix: semgrep error --- .../subcontract_order_summary.py | 28 ++++++++----------- erpnext/controllers/subcontracting.py | 6 ++-- .../stock/doctype/stock_entry/stock_entry.py | 3 +- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 8b08d2a284..0b14e119ab 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -104,26 +104,26 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 140 + "width": 160 }, { - "label": _("Qty"), + "label": _("Order Qty"), "fieldname": "qty", "fieldtype": "Float", - "width": 70 + "width": 90 }, { - "label": _("Received"), + "label": _("Received Qty"), "fieldname": "received_qty", "fieldtype": "Float", - "width": 80 + "width": 110 }, { "label": _("Supplied Item"), "fieldname": "rm_item_code", "fieldtype": "Link", "options": "Item", - "width": 140 + "width": 160 }, { "label": _("Required Qty"), @@ -138,21 +138,15 @@ def get_columns(): "width": 110 }, { - "label": _("Returned Qty"), - "fieldname": "returned_qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Total Supplied"), - "fieldname": "total_supplied_qty", + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120 }, { - "label": _("Consumed Qty"), - "fieldname": "consumed_qty", + "label": _("Returned Qty"), + "fieldname": "returned_qty", "fieldtype": "Float", "width": 110 - }, + } ] \ No newline at end of file diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index fe775766da..a9a38bd02d 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -101,9 +101,9 @@ class Subcontracting(object): self.set_alternative_item_details(row) for doctype in ['Purchase Receipt', 'Purchase Invoice']: - self.remove_consumed_materials(doctype) + self.update_consumed_materials(doctype) - def remove_consumed_materials(self, doctype, return_consumed_items=False): + def update_consumed_materials(self, doctype, return_consumed_items=False): '''Deduct the consumed materials from the available materials.''' pr_items = self.get_received_items(doctype) @@ -311,7 +311,7 @@ class Subcontracting(object): return self.get_purchase_orders() - consumed_items, pr_items = self.remove_consumed_materials(self.doctype, return_consumed_items=True) + consumed_items, pr_items = self.update_consumed_materials(self.doctype, return_consumed_items=True) itemwise_consumed_qty = defaultdict(float) for row in consumed_items: diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 213280870a..0009926f5d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1291,7 +1291,8 @@ class StockEntry(StockController): item_dict[item]["qty"] = 0 # delete items with 0 qty - for item in item_dict.keys(): + list_of_items = item_dict.keys() + for item in list_of_items: if not item_dict[item]["qty"]: del item_dict[item] From 2fb5291785acf9b1e33f2cf8ebe6a4fa2c9ab887 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 7 Jun 2021 21:20:33 +0530 Subject: [PATCH 144/550] fix: toggle consumed qty field based on condition --- .../doctype/purchase_receipt/purchase_receipt.js | 11 +++++++++-- .../doctype/purchase_receipt/purchase_receipt.py | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 887b15a211..cac6bf884b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -41,8 +41,6 @@ frappe.ui.form.on("Purchase Receipt", { } }); - frm.set_df_property('supplied_items', 'cannot_add_rows', 1); - }, onload: function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { @@ -77,6 +75,15 @@ frappe.ui.form.on("Purchase Receipt", { } frm.events.add_custom_buttons(frm); + frm.trigger('toggle_subcontracting_fields'); + }, + + toggle_subcontracting_fields: function(frm) { + frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', + 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); + + frm.set_df_property('supplied_items', 'cannot_add_rows', 1); + frm.set_df_property('supplied_items', 'cannot_delete_rows', 1); }, add_custom_buttons: function(frm) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b8580f95a3..264561f376 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -102,6 +102,11 @@ class PurchaseReceipt(BuyingController): if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): apply_putaway_rule(self.doctype, self.get("items"), self.company) + def onload(self): + super(PurchaseReceipt, self).onload() + self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', + 'backflush_raw_materials_of_subcontract_based_on')) + def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() From ddb0ec261af7470ac2b07f103b24d9386b7b111e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 8 Jun 2021 10:36:39 +0530 Subject: [PATCH 145/550] fix: code cleanup and convert public method to private for subcontracting class --- .../doctype/purchase_order/purchase_order.js | 5 +- .../doctype/purchase_order/purchase_order.py | 13 ++- .../purchase_order_item_supplied.json | 6 +- .../subcontract_order_summary.py | 2 +- erpnext/controllers/subcontracting.py | 106 +++++++++--------- erpnext/public/js/controllers/transaction.js | 4 + .../stock/doctype/stock_entry/stock_entry.py | 10 +- 7 files changed, 76 insertions(+), 70 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 440cde6d9e..233a9c87e5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -74,6 +74,7 @@ frappe.ui.form.on("Purchase Order", { frm.add_custom_button(__('Return of Components'), () => { frm.call({ method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier', + freeze: true, freeze_message: __('Creating Stock Entry'), args: { purchase_order: frm.doc.name, po_details: po_details }, callback: function(r) { @@ -545,14 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( ], primary_action: function() { var data = d.get_values(); - var content_msg = 'Reason for hold: ' + data.reason_for_hold; + let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold; frappe.call({ method: "frappe.desk.form.utils.add_comment", args: { reference_doctype: me.frm.doctype, reference_name: me.frm.docname, - content: __(content_msg), + content: __(reason_for_hold), comment_email: frappe.session.user, comment_by: frappe.session.user_fullname }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 724f863e0f..eaa502ff7f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status from erpnext.stock.utils import get_bin from erpnext.accounts.party import get_party_account_currency -from six import string_types from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details -from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ - unlink_inter_company_doc +from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party, + update_linked_doc, unlink_inter_company_doc) form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -504,7 +503,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions @frappe.whitelist() def make_rm_stock_entry(purchase_order, rm_items): rm_items_list = rm_items - if isinstance(rm_items, string_types): + + if isinstance(rm_items, str): rm_items_list = json.loads(rm_items) elif not rm_items: frappe.throw(_("No Items available for transfer")) @@ -588,7 +588,7 @@ def make_inter_company_sales_order(source_name, target_doc=None): @frappe.whitelist() def get_materials_from_supplier(purchase_order, po_details): - if isinstance(po_details, string_types): + if isinstance(po_details, str): po_details = json.loads(po_details) doc = frappe.get_cached_doc('Purchase Order', purchase_order) @@ -615,7 +615,8 @@ def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_deta if value.batch_no: for batch_no, qty in value.batch_no.items(): - add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) + if qty > 0: + add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) else: add_items_in_ste(ste_doc, value, value.qty, po_details) diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index 505ecd84c5..60247bd90b 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -22,9 +22,9 @@ "required_qty", "supplied_qty", "col_break1", + "consumed_qty", "returned_qty", - "total_supplied_qty", - "consumed_qty" + "total_supplied_qty" ], "fields": [ { @@ -183,7 +183,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-01 00:41:54.123436", + "modified": "2021-06-09 15:17:58.128242", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 0b14e119ab..0c0d4f0531 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -87,7 +87,7 @@ def get_subcontracted_data(po_details, data): def get_columns(): return [ { - "label": _("Id"), + "label": _("Purchase Order"), "fieldname": "po_id", "fieldtype": "Link", "options": "Purchase Order", diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index a9a38bd02d..e81c0f5732 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -1,39 +1,37 @@ -from __future__ import unicode_literals - import frappe from frappe import _ from frappe.utils import flt, cint from collections import defaultdict from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos -class Subcontracting(object): +class Subcontracting(): def set_materials_for_subcontracted_items(self, raw_material_table): if self.doctype == 'Purchase Invoice' and not self.update_stock: return self.raw_material_table = raw_material_table - self.identify_change_in_item_table() - self.prepare_supplied_items() - self.validate_consumed_qty() + self.__identify_change_in_item_table() + self.__prepare_supplied_items() + self.__validate_consumed_qty() - def prepare_supplied_items(self): + def __prepare_supplied_items(self): self.initialized_fields() - self.get_purchase_orders() - self.get_pending_qty_to_receive() + self.__get_purchase_orders() + self.__get_pending_qty_to_receive() self.get_available_materials() - self.remove_changed_rows() - self.set_supplied_items() + self.__remove_changed_rows() + self.__set_supplied_items() def initialized_fields(self): self.available_materials = frappe._dict() self.alternative_item_details = frappe._dict() - self.get_backflush_based_on() + self.__get_backflush_based_on() - def get_backflush_based_on(self): + def __get_backflush_based_on(self): self.backflush_based_on = frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") - def get_purchase_orders(self): + def __get_purchase_orders(self): self.purchase_orders = [] if self.doctype == 'Purchase Order': @@ -41,14 +39,14 @@ class Subcontracting(object): self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] - def identify_change_in_item_table(self): + def __identify_change_in_item_table(self): self.changed_name = [] if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table): self.set(self.raw_material_table, []) return - item_dict = self.get_data_before_save() + item_dict = self.__get_data_before_save() if not item_dict: return True @@ -61,7 +59,7 @@ class Subcontracting(object): self.changed_name.extend(item_dict.keys()) - def get_data_before_save(self): + def __get_data_before_save(self): item_dict = {} if self.doctype == 'Purchase Receipt' and self._doc_before_save: for row in self._doc_before_save.get('items'): @@ -80,7 +78,7 @@ class Subcontracting(object): if not self.purchase_orders: return - for row in self.get_transferred_items(): + for row in self.__get_transferred_items(): key = (row.rm_item_code, row.main_item_code, row.purchase_order) if key not in self.available_materials: @@ -98,20 +96,20 @@ class Subcontracting(object): if row.batch_no: details.batch_no[row.batch_no] += row.qty - self.set_alternative_item_details(row) + self.__set_alternative_item_details(row) for doctype in ['Purchase Receipt', 'Purchase Invoice']: - self.update_consumed_materials(doctype) + self.__update_consumed_materials(doctype) - def update_consumed_materials(self, doctype, return_consumed_items=False): + def __update_consumed_materials(self, doctype, return_consumed_items=False): '''Deduct the consumed materials from the available materials.''' - pr_items = self.get_received_items(doctype) + pr_items = self.__get_received_items(doctype) if not pr_items: return ([], {}) if return_consumed_items else None pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items} - consumed_materials = self.get_consumed_items(doctype, pr_items.keys()) + consumed_materials = self.__get_consumed_items(doctype, pr_items.keys()) if return_consumed_items: return (consumed_materials, pr_items) @@ -130,7 +128,7 @@ class Subcontracting(object): if row.batch_no: self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty - def get_transferred_items(self): + def __get_transferred_items(self): fields = ['`tabStock Entry`.`purchase_order`'] alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'} @@ -149,7 +147,7 @@ class Subcontracting(object): return frappe.get_all('Stock Entry', fields = fields, filters=filters) - def get_received_items(self, doctype): + def __get_received_items(self, doctype): fields = [] self.po_field = 'purchase_order' if doctype == 'Purchase Receipt' else 'po_detail' @@ -162,16 +160,16 @@ class Subcontracting(object): return frappe.get_all(f'{doctype}', fields = fields, filters = filters) - def get_consumed_items(self, doctype, pr_items): + def __get_consumed_items(self, doctype, pr_items): return frappe.get_all(f'{doctype} Item Supplied', fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items))}) - def set_alternative_item_details(self, row): + def __set_alternative_item_details(self, row): if row.get('original_item'): self.alternative_item_details[row.get('original_item')] = row - def get_pending_qty_to_receive(self): + def __get_pending_qty_to_receive(self): '''Get qty to be received against the purchase order.''' self.qty_to_be_received = defaultdict(float) @@ -183,7 +181,7 @@ class Subcontracting(object): self.qty_to_be_received[(row.item_code, row.parent)] += row.qty - def get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item' fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit'] @@ -197,7 +195,7 @@ class Subcontracting(object): return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] - def remove_changed_rows(self): + def __remove_changed_rows(self): if not self.changed_name: return @@ -212,7 +210,7 @@ class Subcontracting(object): i += 1 - def set_supplied_items(self): + def __set_supplied_items(self): self.bom_items = {} has_supplied_items = True if self.get(self.raw_material_table) else False @@ -222,28 +220,28 @@ class Subcontracting(object): continue if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': - for bom_item in self.get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): + for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')): qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor) bom_item.main_item_code = row.item_code - self.update_reserve_warehouse(bom_item, row) - self.set_alternative_item(bom_item) - self.add_supplied_item(row, bom_item, qty) + self.__update_reserve_warehouse(bom_item, row) + self.__set_alternative_item(bom_item) + self.__add_supplied_item(row, bom_item, qty) elif self.backflush_based_on != 'BOM': for key, transfer_item in self.available_materials.items(): if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0: - qty = self.get_qty_based_on_material_transfer(row, transfer_item) or 0 + qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 transfer_item.qty -= qty - self.add_supplied_item(row, transfer_item.get('item_details'), qty) + self.__add_supplied_item(row, transfer_item.get('item_details'), qty) if self.qty_to_be_received: self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty - def update_reserve_warehouse(self, row, item): + def __update_reserve_warehouse(self, row, item): if self.doctype == 'Purchase Order': row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse) - def get_qty_based_on_material_transfer(self, item_row, transfer_item): + def __get_qty_based_on_material_transfer(self, item_row, transfer_item): key = (item_row.item_code, item_row.purchase_order) if self.qty_to_be_received == item_row.qty: @@ -257,11 +255,11 @@ class Subcontracting(object): return qty - def set_alternative_item(self, bom_item): + def __set_alternative_item(self, bom_item): if self.alternative_item_details.get(bom_item.rm_item_code): bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) - def add_supplied_item(self, item_row, bom_item, qty): + def __add_supplied_item(self, item_row, bom_item, qty): bom_item.conversion_factor = item_row.conversion_factor rm_obj = self.append(self.raw_material_table, bom_item) rm_obj.reference_name = item_row.name @@ -269,15 +267,15 @@ class Subcontracting(object): if self.doctype == 'Purchase Order': rm_obj.required_qty = qty else: - self.set_batch_nos(bom_item, item_row, rm_obj, qty) + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) - def set_batch_nos(self, bom_item, item_row, rm_obj, qty): + def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): if batch_qty >= qty: - self.set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) self.available_materials[key]['batch_no'][batch_no] -= qty return @@ -285,18 +283,18 @@ class Subcontracting(object): qty -= batch_qty new_rm_obj = self.append(self.raw_material_table, bom_item) new_rm_obj.reference_name = item_row.name - self.set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) + self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) self.available_materials[key]['batch_no'][batch_no] = 0 else: rm_obj.required_qty = qty rm_obj.consumed_qty = qty - self.set_serial_nos(item_row, rm_obj) + self.__set_serial_nos(item_row, rm_obj) - def set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, 'required_qty': qty}) - self.set_serial_nos(item_row, rm_obj) + self.__set_serial_nos(item_row, rm_obj) - def set_serial_nos(self, item_row, rm_obj): + def __set_serial_nos(self, item_row, rm_obj): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) if (self.available_materials.get(key) and self.available_materials[key]['serial_no']): used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)] @@ -310,17 +308,17 @@ class Subcontracting(object): if self.is_subcontracted != 'Yes': return - self.get_purchase_orders() - consumed_items, pr_items = self.update_consumed_materials(self.doctype, return_consumed_items=True) + self.__get_purchase_orders() + consumed_items, pr_items = self.__update_consumed_materials(self.doctype, return_consumed_items=True) itemwise_consumed_qty = defaultdict(float) for row in consumed_items: key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) itemwise_consumed_qty[key] += row.consumed_qty - self.update_consumed_qty_in_po(itemwise_consumed_qty) + self.__update_consumed_qty_in_po(itemwise_consumed_qty) - def update_consumed_qty_in_po(self, itemwise_consumed_qty): + def __update_consumed_qty_in_po(self, itemwise_consumed_qty): fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name'] filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)} @@ -334,7 +332,7 @@ class Subcontracting(object): itemwise_consumed_qty[key] -= consumed_qty frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) - def validate_consumed_qty(self): + def __validate_consumed_qty(self): for row in self.get(self.raw_material_table): if flt(row.consumed_qty) == 0.0 and row.get('serial_no'): msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 89fed3bf0d..978c8f4879 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -723,6 +723,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var me = this; var item = frappe.get_doc(cdt, cdn); + if (item && item.doctype === 'Purchase Receipt Item Supplied') { + return; + } + if (item && item.serial_no) { if (!item.item_code) { this.frm.trigger("item_code", cdt, cdn); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0009926f5d..66f8b63cb9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1005,10 +1005,12 @@ class StockEntry(StockController): if self.purchase_order and self.purpose == "Send to Subcontractor": #Get PO Supplied Items Details item_wh = frappe._dict(frappe.db.sql(""" - select rm_item_code, reserve_warehouse - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup - where po.name = poitemsup.parent - and po.name = %s""",self.purchase_order)) + SELECT + rm_item_code, reserve_warehouse + FROM + `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup + WHERE + po.name = poitemsup.parent and po.name = %s """,self.purchase_order)) for item in itervalues(item_dict): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): From 5cc3f14506bb75fd525eb774cc5d70a5f4204947 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 15 Jun 2021 17:29:52 +0530 Subject: [PATCH 146/550] fix: purchase invoice qty change not recalculate the consumed qty and added test cases for purchase invoice --- .../purchase_invoice/purchase_invoice.json | 617 +++++------------- .../purchase_invoice/purchase_invoice.py | 2 + erpnext/controllers/buying_controller.py | 5 + erpnext/controllers/subcontracting.py | 41 +- erpnext/public/js/controllers/buying.js | 11 + .../purchase_receipt/purchase_receipt.js | 9 - .../purchase_receipt/purchase_receipt.py | 5 - erpnext/tests/test_subcontracting.py | 267 +++++++- 8 files changed, 461 insertions(+), 496 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index a714ac7827..00ef7d5c18 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -175,9 +175,7 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "naming_series", @@ -189,9 +187,7 @@ "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, - "set_only_once": 1, - "show_days": 1, - "show_seconds": 1 + "set_only_once": 1 }, { "fieldname": "supplier", @@ -203,9 +199,7 @@ "options": "Supplier", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "bold": 1, @@ -217,9 +211,7 @@ "label": "Supplier Name", "oldfieldname": "supplier_name", "oldfieldtype": "Data", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fetch_from": "supplier.tax_id", @@ -227,27 +219,21 @@ "fieldtype": "Read Only", "label": "Tax Id", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "due_date", "fieldtype": "Date", "label": "Due Date", "oldfieldname": "due_date", - "oldfieldtype": "Date", - "show_days": 1, - "show_seconds": 1 + "oldfieldtype": "Date" }, { "default": "0", "fieldname": "is_paid", "fieldtype": "Check", "label": "Is Paid", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", @@ -255,25 +241,19 @@ "fieldtype": "Check", "label": "Is Return (Debit Note)", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break1", "fieldtype": "Column Break", "oldfieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -283,17 +263,13 @@ "label": "Company", "options": "Company", "print_hide": 1, - "remember_last_selected_value": 1, - "show_days": 1, - "show_seconds": 1 + "remember_last_selected_value": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center", - "show_days": 1, - "show_seconds": 1 + "options": "Cost Center" }, { "default": "Today", @@ -305,9 +281,7 @@ "oldfieldtype": "Date", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "posting_time", @@ -316,8 +290,6 @@ "no_copy": 1, "print_hide": 1, "print_width": "100px", - "show_days": 1, - "show_seconds": 1, "width": "100px" }, { @@ -326,9 +298,7 @@ "fieldname": "set_posting_time", "fieldtype": "Check", "label": "Edit Posting Date and Time", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "amended_from", @@ -340,58 +310,44 @@ "oldfieldtype": "Link", "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:doc.on_hold", "fieldname": "sb_14", "fieldtype": "Section Break", - "label": "Hold Invoice", - "show_days": 1, - "show_seconds": 1 + "label": "Hold Invoice" }, { "default": "0", "fieldname": "on_hold", "fieldtype": "Check", - "label": "Hold Invoice", - "show_days": 1, - "show_seconds": 1 + "label": "Hold Invoice" }, { "depends_on": "eval:doc.on_hold", "description": "Once set, this invoice will be on hold till the set date", "fieldname": "release_date", "fieldtype": "Date", - "label": "Release Date", - "show_days": 1, - "show_seconds": 1 + "label": "Release Date" }, { "fieldname": "cb_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "eval:doc.on_hold", "fieldname": "hold_comment", "fieldtype": "Small Text", - "label": "Reason For Putting On Hold", - "show_days": 1, - "show_seconds": 1 + "label": "Reason For Putting On Hold" }, { "collapsible": 1, "collapsible_depends_on": "bill_no", "fieldname": "supplier_invoice_details", "fieldtype": "Section Break", - "label": "Supplier Invoice Details", - "show_days": 1, - "show_seconds": 1 + "label": "Supplier Invoice Details" }, { "fieldname": "bill_no", @@ -399,15 +355,11 @@ "label": "Supplier Invoice No", "oldfieldname": "bill_no", "oldfieldtype": "Data", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_15", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "bill_date", @@ -416,17 +368,13 @@ "no_copy": 1, "oldfieldname": "bill_date", "oldfieldtype": "Date", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "return_against", "fieldname": "returns", "fieldtype": "Section Break", - "label": "Returns", - "show_days": 1, - "show_seconds": 1 + "label": "Returns" }, { "depends_on": "return_against", @@ -436,34 +384,26 @@ "no_copy": 1, "options": "Purchase Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "fieldname": "section_addresses", "fieldtype": "Section Break", - "label": "Address and Contact", - "show_days": 1, - "show_seconds": 1 + "label": "Address and Contact" }, { "fieldname": "supplier_address", "fieldtype": "Link", "label": "Select Supplier Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "address_display", "fieldtype": "Small Text", "label": "Address", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_person", @@ -471,67 +411,51 @@ "in_global_search": 1, "label": "Contact Person", "options": "Contact", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "contact_display", "fieldtype": "Small Text", "label": "Contact", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_mobile", "fieldtype": "Small Text", "label": "Mobile No", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "contact_email", "fieldtype": "Small Text", "label": "Contact Email", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_break_address", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shipping_address", "fieldtype": "Link", "label": "Select Shipping Address", "options": "Address", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "shipping_address_display", "fieldtype": "Small Text", "label": "Shipping Address", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "fieldname": "currency_and_price_list", "fieldtype": "Section Break", "label": "Currency and Price List", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "fieldname": "currency", @@ -540,9 +464,7 @@ "oldfieldname": "currency", "oldfieldtype": "Select", "options": "Currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "conversion_rate", @@ -551,24 +473,18 @@ "oldfieldname": "conversion_rate", "oldfieldtype": "Currency", "precision": "9", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break2", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "buying_price_list", "fieldtype": "Link", "label": "Price List", "options": "Price List", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "price_list_currency", @@ -576,18 +492,14 @@ "label": "Price List Currency", "options": "Currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "plc_conversion_rate", "fieldtype": "Float", "label": "Price List Exchange Rate", "precision": "9", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", @@ -596,15 +508,11 @@ "label": "Ignore Pricing Rule", "no_copy": 1, "permlevel": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "sec_warehouse", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "depends_on": "update_stock", @@ -613,9 +521,7 @@ "fieldtype": "Link", "label": "Set Accepted Warehouse", "options": "Warehouse", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "update_stock", @@ -625,15 +531,11 @@ "label": "Rejected Warehouse", "no_copy": 1, "options": "Warehouse", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "col_break_warehouse", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "No", @@ -641,33 +543,25 @@ "fieldtype": "Select", "label": "Raw Materials Supplied", "options": "No\nYes", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "items_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-shopping-cart" }, { "default": "0", "fieldname": "update_stock", "fieldtype": "Check", "label": "Update Stock", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "scan_barcode", "fieldtype": "Data", - "label": "Scan Barcode", - "show_days": 1, - "show_seconds": 1 + "label": "Scan Barcode" }, { "allow_bulk_edit": 1, @@ -677,56 +571,43 @@ "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Purchase Invoice Item", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "pricing_rule_details", "fieldtype": "Section Break", - "label": "Pricing Rules", - "show_days": 1, - "show_seconds": 1 + "label": "Pricing Rules" }, { "fieldname": "pricing_rules", "fieldtype": "Table", "label": "Pricing Rule Detail", "options": "Pricing Rule Detail", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible_depends_on": "supplied_items", "fieldname": "raw_materials_supplied", "fieldtype": "Section Break", - "label": "Raw Materials Supplied", - "show_days": 1, - "show_seconds": 1 + "label": "Raw Materials Supplied" }, { + "depends_on": "update_stock", "fieldname": "supplied_items", "fieldtype": "Table", "label": "Supplied Items", - "options": "Purchase Receipt Item Supplied", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "options": "Purchase Receipt Item Supplied" }, { "fieldname": "section_break_26", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "total_qty", "fieldtype": "Float", "label": "Total Quantity", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_total", @@ -734,9 +615,7 @@ "label": "Total (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_net_total", @@ -746,24 +625,18 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_28", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "total", "fieldtype": "Currency", "label": "Total", "options": "currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "net_total", @@ -773,56 +646,42 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_net_weight", "fieldtype": "Float", "label": "Total Net Weight", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "taxes_section", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-money" }, { "fieldname": "tax_category", "fieldtype": "Link", "label": "Tax Category", "options": "Tax Category", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_49", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "shipping_rule", "fieldtype": "Link", "label": "Shipping Rule", "options": "Shipping Rule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "section_break_51", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "taxes_and_charges", @@ -831,9 +690,7 @@ "oldfieldname": "purchase_other_charges", "oldfieldtype": "Link", "options": "Purchase Taxes and Charges Template", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "taxes", @@ -841,17 +698,13 @@ "label": "Purchase Taxes and Charges", "oldfieldname": "purchase_tax_details", "oldfieldtype": "Table", - "options": "Purchase Taxes and Charges", - "show_days": 1, - "show_seconds": 1 + "options": "Purchase Taxes and Charges" }, { "collapsible": 1, "fieldname": "sec_tax_breakup", "fieldtype": "Section Break", - "label": "Tax Breakup", - "show_days": 1, - "show_seconds": 1 + "label": "Tax Breakup" }, { "fieldname": "other_charges_calculation", @@ -860,17 +713,13 @@ "no_copy": 1, "oldfieldtype": "HTML", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "totals", "fieldtype": "Section Break", "oldfieldtype": "Section Break", - "options": "fa fa-money", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-money" }, { "fieldname": "base_taxes_and_charges_added", @@ -880,9 +729,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_taxes_and_charges_deducted", @@ -892,9 +739,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_total_taxes_and_charges", @@ -904,15 +749,11 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_40", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "taxes_and_charges_added", @@ -922,9 +763,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "taxes_and_charges_deducted", @@ -934,9 +773,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_taxes_and_charges", @@ -944,18 +781,14 @@ "label": "Total Taxes and Charges", "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, "collapsible_depends_on": "discount_amount", "fieldname": "section_break_44", "fieldtype": "Section Break", - "label": "Additional Discount", - "show_days": 1, - "show_seconds": 1 + "label": "Additional Discount" }, { "default": "Grand Total", @@ -963,9 +796,7 @@ "fieldtype": "Select", "label": "Apply Additional Discount On", "options": "\nGrand Total\nNet Total", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_discount_amount", @@ -973,38 +804,28 @@ "label": "Additional Discount Amount (Company Currency)", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_46", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "additional_discount_percentage", "fieldtype": "Float", "label": "Additional Discount Percentage", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "discount_amount", "fieldtype": "Currency", "label": "Additional Discount Amount", "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "section_break_49", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "base_grand_total", @@ -1014,9 +835,7 @@ "oldfieldtype": "Currency", "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1026,9 +845,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1038,9 +855,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "base_in_words", @@ -1050,17 +865,13 @@ "oldfieldname": "in_words", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break8", "fieldtype": "Column Break", "oldfieldtype": "Column Break", "print_hide": 1, - "show_days": 1, - "show_seconds": 1, "width": "50%" }, { @@ -1071,9 +882,7 @@ "oldfieldname": "grand_total_import", "oldfieldtype": "Currency", "options": "currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1083,9 +892,7 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:!doc.disable_rounded_total", @@ -1095,9 +902,7 @@ "no_copy": 1, "options": "currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "in_words", @@ -1107,9 +912,7 @@ "oldfieldname": "in_words_import", "oldfieldtype": "Data", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "total_advance", @@ -1120,9 +923,7 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "outstanding_amount", @@ -1133,18 +934,14 @@ "oldfieldtype": "Currency", "options": "party_account_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "depends_on": "grand_total", "fieldname": "disable_rounded_total", "fieldtype": "Check", - "label": "Disable Rounded Total", - "show_days": 1, - "show_seconds": 1 + "label": "Disable Rounded Total" }, { "collapsible": 1, @@ -1152,26 +949,20 @@ "depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)", "fieldname": "payments_section", "fieldtype": "Section Break", - "label": "Payments", - "show_days": 1, - "show_seconds": 1 + "label": "Payments" }, { "fieldname": "mode_of_payment", "fieldtype": "Link", "label": "Mode of Payment", "options": "Mode of Payment", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "fieldname": "clearance_date", @@ -1179,15 +970,11 @@ "label": "Clearance Date", "no_copy": 1, "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_br_payments", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "is_paid", @@ -1196,9 +983,7 @@ "label": "Paid Amount", "no_copy": 1, "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_paid_amount", @@ -1207,9 +992,7 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, @@ -1217,9 +1000,7 @@ "depends_on": "grand_total", "fieldname": "write_off", "fieldtype": "Section Break", - "label": "Write Off", - "show_days": 1, - "show_seconds": 1 + "label": "Write Off" }, { "fieldname": "write_off_amount", @@ -1227,9 +1008,7 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "base_write_off_amount", @@ -1238,15 +1017,11 @@ "no_copy": 1, "options": "Company:company:default_currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "column_break_61", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1254,9 +1029,7 @@ "fieldtype": "Link", "label": "Write Off Account", "options": "Account", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "depends_on": "eval:flt(doc.write_off_amount)!=0", @@ -1264,9 +1037,7 @@ "fieldtype": "Link", "label": "Write Off Cost Center", "options": "Cost Center", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -1276,17 +1047,13 @@ "label": "Advance Payments", "oldfieldtype": "Section Break", "options": "fa fa-money", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "default": "0", "fieldname": "allocate_advances_automatically", "fieldtype": "Check", - "label": "Set Advances and Allocate (FIFO)", - "show_days": 1, - "show_seconds": 1 + "label": "Set Advances and Allocate (FIFO)" }, { "depends_on": "eval:!doc.allocate_advances_automatically", @@ -1294,9 +1061,7 @@ "fieldtype": "Button", "label": "Get Advances Paid", "oldfieldtype": "Button", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "advances", @@ -1306,26 +1071,20 @@ "oldfieldname": "advance_allocation_details", "oldfieldtype": "Table", "options": "Purchase Invoice Advance", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, "collapsible_depends_on": "eval:(!doc.is_return)", "fieldname": "payment_schedule_section", "fieldtype": "Section Break", - "label": "Payment Terms", - "show_days": 1, - "show_seconds": 1 + "label": "Payment Terms" }, { "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", - "options": "Payment Terms Template", - "show_days": 1, - "show_seconds": 1 + "options": "Payment Terms Template" }, { "fieldname": "payment_schedule", @@ -1333,9 +1092,7 @@ "label": "Payment Schedule", "no_copy": 1, "options": "Payment Schedule", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, @@ -1343,33 +1100,25 @@ "fieldname": "terms_section_break", "fieldtype": "Section Break", "label": "Terms and Conditions", - "options": "fa fa-legal", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-legal" }, { "fieldname": "tc_name", "fieldtype": "Link", "label": "Terms", "options": "Terms and Conditions", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "terms", "fieldtype": "Text Editor", - "label": "Terms and Conditions1", - "show_days": 1, - "show_seconds": 1 + "label": "Terms and Conditions1" }, { "collapsible": 1, "fieldname": "printing_settings", "fieldtype": "Section Break", - "label": "Printing Settings", - "show_days": 1, - "show_seconds": 1 + "label": "Printing Settings" }, { "allow_on_submit": 1, @@ -1377,9 +1126,7 @@ "fieldtype": "Link", "label": "Letter Head", "options": "Letter Head", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1387,15 +1134,11 @@ "fieldname": "group_same_items", "fieldtype": "Check", "label": "Group same items", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_112", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "allow_on_submit": 1, @@ -1407,18 +1150,14 @@ "oldfieldtype": "Link", "options": "Print Heading", "print_hide": 1, - "report_hide": 1, - "show_days": 1, - "show_seconds": 1 + "report_hide": 1 }, { "fieldname": "language", "fieldtype": "Data", "label": "Print Language", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "collapsible": 1, @@ -1427,9 +1166,7 @@ "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "credit_to", @@ -1440,9 +1177,7 @@ "options": "Account", "print_hide": 1, "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "party_account_currency", @@ -1452,9 +1187,7 @@ "no_copy": 1, "options": "Currency", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "No", @@ -1464,9 +1197,7 @@ "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "against_expense_account", @@ -1476,15 +1207,11 @@ "no_copy": 1, "oldfieldname": "against_expense_account", "oldfieldtype": "Small Text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_63", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "Draft", @@ -1493,9 +1220,7 @@ "in_standard_filter": 1, "label": "Status", "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "inter_company_invoice_reference", @@ -1504,9 +1229,7 @@ "no_copy": 1, "options": "Sales Invoice", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "remarks", @@ -1515,18 +1238,14 @@ "no_copy": 1, "oldfieldname": "remarks", "oldfieldtype": "Text", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1535,9 +1254,7 @@ "fieldtype": "Date", "label": "From Date", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "allow_on_submit": 1, @@ -1546,15 +1263,11 @@ "fieldtype": "Date", "label": "To Date", "no_copy": 1, - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_114", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "auto_repeat", @@ -1563,32 +1276,24 @@ "no_copy": 1, "options": "Auto Repeat", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "allow_on_submit": 1, "depends_on": "eval: doc.auto_repeat", "fieldname": "update_auto_repeat_reference", "fieldtype": "Button", - "label": "Update Auto Repeat Reference", - "show_days": 1, - "show_seconds": 1 + "label": "Update Auto Repeat Reference" }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions ", - "show_days": 1, - "show_seconds": 1 + "label": "Accounting Dimensions " }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "0", @@ -1596,9 +1301,7 @@ "fieldname": "is_internal_supplier", "fieldtype": "Check", "label": "Is Internal Supplier", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "tax_withholding_category", @@ -1606,25 +1309,19 @@ "hidden": 1, "label": "Tax Withholding Category", "options": "Tax Withholding Category", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "billing_address", "fieldtype": "Link", "label": "Select Billing Address", - "options": "Address", - "show_days": 1, - "show_seconds": 1 + "options": "Address" }, { "fieldname": "billing_address_display", "fieldtype": "Small Text", "label": "Billing Address", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "project", @@ -1638,9 +1335,7 @@ "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", - "options": "Account", - "show_days": 1, - "show_seconds": 1 + "options": "Account" }, { "depends_on": "eval:doc.is_internal_supplier", @@ -1649,9 +1344,7 @@ "fieldname": "represents_company", "fieldtype": "Link", "label": "Represents Company", - "options": "Company", - "show_days": 1, - "show_seconds": 1 + "options": "Company" }, { "depends_on": "eval:doc.update_stock && doc.is_internal_supplier", @@ -1663,8 +1356,6 @@ "options": "Warehouse", "print_hide": 1, "print_width": "50px", - "show_days": 1, - "show_seconds": 1, "width": "50px" }, { @@ -1692,7 +1383,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-06-09 12:30:25.632109", + "modified": "2021-06-15 18:20:56.806195", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0ee0bc7e11..45d89ad1c8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController): # because updating ordered qty in bin depends upon updated ordered qty in PO if self.update_stock == 1: self.update_stock_ledger() + self.set_consumed_qty_in_po() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") @@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.update_stock_ledger() self.delete_auto_created_batches() + self.set_consumed_qty_in_po() self.make_gl_entries_on_cancel() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 1907885717..0b0da5f413 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -58,6 +58,11 @@ class BuyingController(StockController, Subcontracting): if self.doctype in ("Purchase Receipt", "Purchase Invoice"): self.update_valuation_rate() + def onload(self): + super(BuyingController, self).onload() + self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', + 'backflush_raw_materials_of_subcontract_based_on')) + def set_missing_values(self, for_validate=False): super(BuyingController, self).set_missing_values(for_validate) diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index e81c0f5732..db841626a5 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -40,9 +40,10 @@ class Subcontracting(): self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] def __identify_change_in_item_table(self): - self.changed_name = [] + self.__changed_name = [] + self.__reference_name = [] - if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table): + if self.doctype == 'Purchase Order' or self.is_new(): self.set(self.raw_material_table, []) return @@ -51,17 +52,18 @@ class Subcontracting(): return True for n_row in self.items: + self.__reference_name.append(n_row.name) if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]: - self.changed_name.append(n_row.name) + self.__changed_name.append(n_row.name) if item_dict.get(n_row.name): del item_dict[n_row.name] - self.changed_name.extend(item_dict.keys()) + self.__changed_name.extend(item_dict.keys()) def __get_data_before_save(self): item_dict = {} - if self.doctype == 'Purchase Receipt' and self._doc_before_save: + if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save: for row in self._doc_before_save.get('items'): item_dict[row.name] = (row.item_code, row.qty) @@ -149,7 +151,7 @@ class Subcontracting(): def __get_received_items(self, doctype): fields = [] - self.po_field = 'purchase_order' if doctype == 'Purchase Receipt' else 'po_detail' + self.po_field = 'purchase_order' for field in ['name', self.po_field, 'parent']: fields.append(f'`tab{doctype} Item`.`{field}`') @@ -161,9 +163,9 @@ class Subcontracting(): return frappe.get_all(f'{doctype}', fields = fields, filters = filters) def __get_consumed_items(self, doctype, pr_items): - return frappe.get_all(f'{doctype} Item Supplied', + return frappe.get_all('Purchase Receipt Item Supplied', fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'], - filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items))}) + filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype}) def __set_alternative_item_details(self, row): if row.get('original_item'): @@ -196,13 +198,16 @@ class Subcontracting(): return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or [] def __remove_changed_rows(self): - if not self.changed_name: + if not self.__changed_name: return i=1 self.set(self.raw_material_table, []) for d in self._doc_before_save.supplied_items: - if d.reference_name in self.changed_name: + if d.reference_name in self.__changed_name: + continue + + if (d.reference_name not in self.__reference_name): continue d.idx = i @@ -215,8 +220,8 @@ class Subcontracting(): has_supplied_items = True if self.get(self.raw_material_table) else False for row in self.items: - if (self.doctype != 'Purchase Order' and ((self.changed_name and row.name not in self.changed_name) - or (has_supplied_items and not self.changed_name))): + if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name) + or (has_supplied_items and not self.__changed_name))): continue if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM': @@ -305,16 +310,18 @@ class Subcontracting(): self.available_materials[key]['serial_no'].remove(sn) def set_consumed_qty_in_po(self): + # Update consumed qty back in the purchase order if self.is_subcontracted != 'Yes': return self.__get_purchase_orders() - consumed_items, pr_items = self.__update_consumed_materials(self.doctype, return_consumed_items=True) - itemwise_consumed_qty = defaultdict(float) - for row in consumed_items: - key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) - itemwise_consumed_qty[key] += row.consumed_qty + for doctype in ['Purchase Receipt', 'Purchase Invoice']: + consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True) + + for row in consumed_items: + key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) + itemwise_consumed_qty[key] += row.consumed_qty self.__update_consumed_qty_in_po(itemwise_consumed_qty) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index e7dcd41068..5c9f5d7da4 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -122,9 +122,20 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ this.set_from_product_bundle(); } + this.toggle_subcontracting_fields(); this._super(); }, + toggle_subcontracting_fields: function() { + if (in_list(['Purchase Receipt', 'Purchase Invoice'], this.frm.doc.doctype)) { + this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', + 'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM'); + + this.frm.set_df_property('supplied_items', 'cannot_add_rows', 1); + this.frm.set_df_property('supplied_items', 'cannot_delete_rows', 1); + } + }, + supplier: function() { var me = this; erpnext.utils.get_party_details(this.frm, null, null, function(){ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index cac6bf884b..befdad9692 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -75,15 +75,6 @@ frappe.ui.form.on("Purchase Receipt", { } frm.events.add_custom_buttons(frm); - frm.trigger('toggle_subcontracting_fields'); - }, - - toggle_subcontracting_fields: function(frm) { - frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', - 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); - - frm.set_df_property('supplied_items', 'cannot_add_rows', 1); - frm.set_df_property('supplied_items', 'cannot_delete_rows', 1); }, add_custom_buttons: function(frm) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 264561f376..b8580f95a3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -102,11 +102,6 @@ class PurchaseReceipt(BuyingController): if self.get("items") and self.apply_putaway_rule and not self.get("is_return"): apply_putaway_rule(self.doctype, self.get("items"), self.company) - def onload(self): - super(PurchaseReceipt, self).onload() - self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings', - 'backflush_raw_materials_of_subcontract_based_on')) - def validate(self): self.validate_posting_time() super(PurchaseReceipt, self).validate() diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index c1a458a6dd..d2438f8c60 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -10,7 +10,7 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.purchase_order import (make_rm_stock_entry, - make_purchase_receipt, get_materials_from_supplier) + make_purchase_receipt, make_purchase_invoice, get_materials_from_supplier) class TestSubcontracting(unittest.TestCase): def setUp(self): @@ -458,10 +458,273 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, details.qty) self.assertEqual(value.batch_no, details.batch_no) + + def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + self.assertEqual(value.qty, qty) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].qty = 2 + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + + def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA2' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 5 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3])) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA6' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 5 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + + transferred_batch_no = '' + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + transferred_batch_no = details.batch_no + self.assertEqual(value.batch_no, details.batch_no) + + pr1.load_from_db() + pr1.supplied_items[0].consumed_qty = 5 + pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0] + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + def test_item_with_batch_based_on_bom_for_purchase_invoice(self): + ''' + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + ''' + + set_backflush_based_on('BOM') + item_code = 'Subcontracted Item SA4' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, + {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + ] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + add_second_row_in_pr(pr1) + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 4) + + pr1 = make_purchase_invoice(po.name) + pr1.update_stock = 1 + pr1.items[0].qty = 2 + pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.save() + pr1.submit() + + for key, value in get_supplied_items(pr1).items(): + self.assertEqual(value.qty, 2) + def add_second_row_in_pr(pr): item_dict = {} for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom', - 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate']: + 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate', 'expense_account', 'po_detail']: item_dict[column] = pr.items[0].get(column) pr.append('items', item_dict) From f5db407461c1ee898a84d83587cf6f3eae3791c1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 18 Jun 2021 20:37:42 +0530 Subject: [PATCH 147/550] fix: available qty for consumption --- .../purchase_order/test_purchase_order.py | 3 - .../purchase_receipt_item_supplied.json | 20 ++++-- erpnext/controllers/buying_controller.py | 10 +-- erpnext/controllers/subcontracting.py | 66 ++++++++++++++++--- erpnext/stock/stock_ledger.py | 2 +- erpnext/tests/test_subcontracting.py | 31 +++++++++ 6 files changed, 110 insertions(+), 22 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 33d1971451..8563b97ab7 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -847,9 +847,6 @@ class TestPurchaseOrder(unittest.TestCase): for item in rm_items: transferred_rm_map[item.get('rm_item_code')] = item - for item in pr.get('supplied_items'): - self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty) - update_backflush_based_on("BOM") def test_supplied_qty_against_subcontracted_po(self): diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index d8c37f5881..f9cd72015a 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -26,7 +26,8 @@ "secbreak_3", "batch_no", "col_break4", - "serial_no" + "serial_no", + "purchase_order" ], "fields": [ { @@ -81,9 +82,10 @@ "fieldname": "required_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", + "label": "Available Qty For Consumption", "oldfieldname": "required_qty", "oldfieldtype": "Currency", + "print_hide": 1, "read_only": 1 }, { @@ -91,7 +93,7 @@ "fieldname": "consumed_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Consumed Qty", + "label": "Qty to Be Consumed", "oldfieldname": "consumed_qty", "oldfieldtype": "Currency", "reqd": 1 @@ -190,12 +192,22 @@ "fieldtype": "Data", "label": "Item Name", "read_only": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "hidden": 1, + "label": "Purchase Order", + "no_copy": 1, + "options": "Purchase Order", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-29 17:22:14.977117", + "modified": "2021-06-19 19:33:04.431213", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 0b0da5f413..6a550e0e97 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -292,11 +292,13 @@ class BuyingController(StockController, Subcontracting): if item in self.sub_contracted_items and not item.bom: frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) - if self.doctype == "Purchase Order": - for supplied_item in self.get("supplied_items"): - if not supplied_item.reserve_warehouse: - frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code))) + if self.doctype != "Purchase Order": + return + for row in self.get("supplied_items"): + if not row.reserve_warehouse: + msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" + frappe.throw(_(msg)) else: for item in self.get("items"): if item.bom: diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index db841626a5..36ae110216 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -1,6 +1,7 @@ import frappe +import copy from frappe import _ -from frappe.utils import flt, cint +from frappe.utils import flt, cint, get_link_to_form from collections import defaultdict from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -12,7 +13,7 @@ class Subcontracting(): self.raw_material_table = raw_material_table self.__identify_change_in_item_table() self.__prepare_supplied_items() - self.__validate_consumed_qty() + self.__validate_supplied_items() def __prepare_supplied_items(self): self.initialized_fields() @@ -24,6 +25,7 @@ class Subcontracting(): def initialized_fields(self): self.available_materials = frappe._dict() + self.__transferred_items = frappe._dict() self.alternative_item_details = frappe._dict() self.__get_backflush_based_on() @@ -100,6 +102,7 @@ class Subcontracting(): self.__set_alternative_item_details(row) + self.__transferred_items = copy.deepcopy(self.available_materials) for doctype in ['Purchase Receipt', 'Purchase Invoice']: self.__update_consumed_materials(doctype) @@ -254,6 +257,8 @@ class Subcontracting(): if self.qty_to_be_received: qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + transfer_item.item_details.required_qty = transfer_item.qty + if (transfer_item.serial_no or frappe.get_cached_value('UOM', transfer_item.item_details.stock_uom, 'must_be_whole_number')): return frappe.utils.ceil(qty) @@ -272,12 +277,15 @@ class Subcontracting(): if self.doctype == 'Purchase Order': rm_obj.required_qty = qty else: + rm_obj.consumed_qty = 0 + rm_obj.purchase_order = item_row.purchase_order self.__set_batch_nos(bom_item, item_row, rm_obj, qty) def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) if (self.available_materials.get(key) and self.available_materials[key]['batch_no']): + new_rm_obj = None for batch_no, batch_qty in self.available_materials[key]['batch_no'].items(): if batch_qty >= qty: self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) @@ -290,13 +298,21 @@ class Subcontracting(): new_rm_obj.reference_name = item_row.name self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) self.available_materials[key]['batch_no'][batch_no] = 0 + + if abs(qty) > 0 and not new_rm_obj: + self.__set_consumed_qty(rm_obj, qty) else: - rm_obj.required_qty = qty - rm_obj.consumed_qty = qty + self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) self.__set_serial_nos(item_row, rm_obj) + def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): + rm_obj.required_qty = required_qty + rm_obj.consumed_qty = consumed_qty + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): - rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, 'required_qty': qty}) + rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no, + 'required_qty': qty, 'purchase_order': item_row.purchase_order}) + self.__set_serial_nos(item_row, rm_obj) def __set_serial_nos(self, item_row, rm_obj): @@ -339,9 +355,39 @@ class Subcontracting(): itemwise_consumed_qty[key] -= consumed_qty frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty) - def __validate_consumed_qty(self): - for row in self.get(self.raw_material_table): - if flt(row.consumed_qty) == 0.0 and row.get('serial_no'): - msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + def __validate_supplied_items(self): + if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']: + return - frappe.throw(_(msg),title=_('Consumed Items Qty Check')) \ No newline at end of file + for row in self.get(self.raw_material_table): + self.__validate_consumed_qty(row) + + key = (row.rm_item_code, row.main_item_code, row.purchase_order) + if not self.__transferred_items or not self.__transferred_items.get(key): + return + + self.__validate_batch_no(row, key) + self.__validate_serial_no(row, key) + + def __validate_consumed_qty(self, row): + if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0: + msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' + + frappe.throw(_(msg),title=_('Consumed Items Qty Check')) + + def __validate_batch_no(self, row, key): + if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'): + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) + + def __validate_serial_no(self, row, key): + if row.get('serial_no'): + serial_nos = get_serial_nos(row.get('serial_no')) + incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no')) + + if incorrect_sn: + incorrect_sn = "\n".join(incorrect_sn) + link = get_link_to_form('Purchase Order', row.purchase_order) + msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}' + frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) \ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fb2ecab249..9fe89c3fa5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -485,7 +485,7 @@ class update_entries_after(object): # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': - doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no) + doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) for d in (doc.items + doc.supplied_items): d.db_update() diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index d2438f8c60..8b0ce0957d 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -395,6 +395,37 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, details.qty) self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + def test_incorrect_serial_no_components_based_on_material_transfer(self): + ''' + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the serialized componenets to the supplier. + - Create purchase receipt and change the serial no which is not transferred. + - System should throw the error and not allowed to save the purchase receipt. + ''' + + set_backflush_based_on('Material Transferred for Subcontract') + item_code = 'Subcontracted Item SA2' + items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + + rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 10}] + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + po = create_purchase_order(rm_items = items, is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC") + + for d in rm_items: + d['po_detail'] = po.items[0].name + + make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, + rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + + pr1 = make_purchase_receipt(po.name) + pr1.save() + pr1.supplied_items[0].serial_no = 'ABCD' + self.assertRaises(frappe.ValidationError, pr1.save) + pr1.delete() + def test_partial_transfer_batch_based_on_material_transfer(self): ''' - Set backflush based on Material Transferred for Subcontract From 3d7f660bec36faf4c73dab15c7e6974e4473591f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 20 Jun 2021 10:20:35 +0530 Subject: [PATCH 148/550] fix: test case for Project Profitability report --- .../project_profitability/test_project_profitability.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index ea6bdb54ca..180926fe25 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import getdate, nowdate +from frappe.utils import getdate, nowdate, add_days from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice @@ -16,17 +16,22 @@ class TestProjectProfitability(unittest.TestCase): make_salary_structure_for_timesheet(emp, company='_Test Company') self.timesheet = make_timesheet(emp, simulate = True, is_billable=1) self.salary_slip = make_salary_slip(self.timesheet.name) + holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate()) + if holidays: + frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1) + self.salary_slip.submit() self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') self.sales_invoice.due_date = nowdate() self.sales_invoice.submit() frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) + frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 0) def test_project_profitability(self): filters = { 'company': '_Test Company', - 'start_date': getdate(), + 'start_date': add_days(getdate(), -3), 'end_date': getdate() } From 740df95c581446991b8c338454f4dc9b6e61dcee Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sun, 20 Jun 2021 17:44:35 +0530 Subject: [PATCH 149/550] fix(Asset Repair): Set completion_date on changing repair_status to 'Completed' --- erpnext/assets/doctype/asset_repair/asset_repair.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index efa6a9d494..ced3dad1e5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -28,6 +28,10 @@ frappe.ui.form.on('Asset Repair', { } }); } + + if (frm.doc.repair_status == "Completed") { + frm.set_value('completion_date', frappe.datetime.now_datetime()); + } } }); From 582f18772632d98ab138390532aa52a836aa9ef3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jun 2021 00:59:02 +0530 Subject: [PATCH 150/550] fix: rate not able to change in purchase order --- erpnext/controllers/taxes_and_totals.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2bb83ea7f0..56da5b71da 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object): item.margin_type = None item.margin_rate_or_amount = 0.0 - if item.margin_type and item.margin_rate_or_amount: + if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate): + item.margin_type = "Amount" + item.margin_rate_or_amount = flt(item.rate - item.price_list_rate, + item.precision("margin_rate_or_amount")) + item.rate_with_margin = item.rate + + elif item.margin_type and item.margin_rate_or_amount: margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 rate_with_margin = flt(item.price_list_rate) + flt(margin_value) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) From fb89008a13a49e57825228bd8a179c9e8e963aa2 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 21 Jun 2021 10:49:09 +0530 Subject: [PATCH 151/550] fix(pos): unsupported operand type -=: for 'float' and 'NoneType' (#26097) --- .../accounts/doctype/accounts_settings/accounts_settings.json | 4 ++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/public/js/controllers/transaction.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 0ff7230e55..703e93c075 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -260,7 +260,7 @@ "description": "If enabled, ledger entries will be posted for change amount in POS transactions", "fieldname": "post_change_gl_entries", "fieldtype": "Check", - "label": "Change Ledger Entries for Change Amount" + "label": "Create Ledger Entries for Change Amount" } ], "icon": "icon-cog", @@ -268,7 +268,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-16 13:14:45.739107", + "modified": "2021-06-17 20:26:03.721202", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e14f305fc5..55a5b99907 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -989,7 +989,7 @@ class SalesInvoice(SellingController): for payment_mode in self.payments: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: - payment_mode.base_amount -= self.change_amount + payment_mode.base_amount -= flt(self.change_amount) if payment_mode.amount: # POS, make payment entries diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 978c8f4879..6dc40f05e7 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -387,7 +387,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(this.frm.doc.scan_barcode) { frappe.call({ - method: "erpnext.selling.page.point_of_sale.point_of_sale.search_serial_or_batch_or_barcode_number", + method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number", args: { search_value: this.frm.doc.scan_barcode } }).then(r => { const data = r && r.message; From 4b32ccb1245ff8256b776cc992da64b5e37cd737 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 21 Jun 2021 10:49:25 +0530 Subject: [PATCH 152/550] fix(pos): unsupported operand type -=: for 'float' and 'NoneType' (#26097) From e78364c1917e0eeccd45587221fc51f00e185586 Mon Sep 17 00:00:00 2001 From: Ankush Date: Mon, 21 Jun 2021 11:15:16 +0530 Subject: [PATCH 153/550] fix: status indicator for delivery notes (#26062) On list view `per_returned` isn't fetched i.e. `undefined` which become 0 hence the list view indicator is false. This "computation" is already done by status updater, so relying on doc.status is better than redefining it. --- erpnext/stock/doctype/delivery_note/delivery_note_list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index f08125b199..0402898047 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -6,8 +6,8 @@ frappe.listview_settings['Delivery Note'] = { return [__("Return"), "gray", "is_return,=,Yes"]; } else if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; - } else if (flt(doc.per_returned, 2) === 100) { - return [__("Return Issued"), "grey", "per_returned,=,100"]; + } else if (doc.status === "Return Issued") { + return [__("Return Issued"), "grey", "status,=,Return Issued"]; } else if (flt(doc.per_billed, 2) < 100) { return [__("To Bill"), "orange", "per_billed,<,100"]; } else if (flt(doc.per_billed, 2) === 100) { From b081d7933262933dd8f31668d4d2ff1d15df5d12 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 14:52:00 +0530 Subject: [PATCH 154/550] fix(Asset Repair): Fix tests --- .../doctype/asset_repair/asset_repair.json | 2 +- .../doctype/asset_repair/asset_repair.py | 58 +++++++++---------- .../doctype/asset_repair/test_asset_repair.py | 7 ++- 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 6f9b6863f7..c25216c21b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -264,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-19 15:20:24.056706", + "modified": "2021-06-20 17:35:51.075537", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 212af7a930..5fccfb76a5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -5,74 +5,73 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import time_diff_in_hours, getdate, nowdate, add_months, flt, cint -from frappe.model.document import Document +from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.controllers.accounts_controller import AccountsController class AssetRepair(AccountsController): def validate(self): - if self.repair_status == "Completed" and not self.completion_date: - self.completion_date = nowdate() - + self.asset_doc = frappe.get_doc('Asset', self.asset) self.update_status() - self.set_total_value() # change later + if self.get('stock_items'): + self.set_total_value() # change later self.calculate_total_repair_cost() def update_status(self): if self.repair_status == 'Pending': frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order') else: - asset = frappe.get_doc('Asset', self.asset) - asset.set_status() + self.asset_doc.set_status() def set_total_value(self): - for item in self.stock_items: + for item in self.get('stock_items'): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost - for item in self.stock_items: - self.total_repair_cost += item.total_value + if self.get('stock_items'): + for item in self.get('stock_items'): + self.total_repair_cost += item.total_value def on_submit(self): self.check_repair_status() - if self.stock_consumption or self.capitalize_repair_cost: + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): self.increase_asset_value() - if self.stock_consumption: + if self.get('stock_consumption'): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() - if self.capitalize_repair_cost: + if self.get('capitalize_repair_cost'): self.make_gl_entries() if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: self.modify_depreciation_schedule() + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.save() + def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) def check_for_stock_items_and_warehouse(self): - if not self.stock_items: + if not self.get('stock_items'): frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) if not self.warehouse: frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def increase_asset_value(self): total_value_of_stock_consumed = 0 - for item in self.stock_items: - total_value_of_stock_consumed += item.total_value + if self.get('stock_consumption'): + for item in self.get('stock_items'): + total_value_of_stock_consumed += item.total_value - asset = frappe.get_doc('Asset', self.asset) - asset.flags.ignore_validate_update_after_submit = True - if asset.calculate_depreciation: - for row in asset.finance_books: + if self.asset_doc.calculate_depreciation: + for row in self.asset_doc.finance_books: row.value_after_depreciation += total_value_of_stock_consumed if self.capitalize_repair_cost: row.value_after_depreciation += self.repair_cost - asset.save() def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ @@ -81,7 +80,7 @@ class AssetRepair(AccountsController): "company": self.company }) - for stock_item in self.stock_items: + for stock_item in self.get('stock_items'): stock_entry.append('items', { "s_warehouse": self.warehouse, "item_code": stock_item.item, @@ -121,7 +120,7 @@ class AssetRepair(AccountsController): }, item=self) ) - if self.stock_consumption: + if self.get('stock_consumption'): # creating GL Entries for each row in Stock Items based on the Stock Entry created for it stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) for item in stock_entry.items: @@ -158,18 +157,13 @@ class AssetRepair(AccountsController): return gl_entries def modify_depreciation_schedule(self): - asset = frappe.get_doc('Asset', self.asset) - asset.flags.ignore_validate_update_after_submit = True - for row in asset.finance_books: + for row in self.asset_doc.finance_books: row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation - asset.flags.increase_in_asset_life = False + self.asset_doc.flags.increase_in_asset_life = False extra_months = self.increase_in_asset_life % row.frequency_of_depreciation if extra_months != 0: - self.calculate_last_schedule_date(asset, row, extra_months) - - asset.prepare_depreciation_data() - asset.save() + self.calculate_last_schedule_date(self.asset_doc, row, extra_months) # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation def calculate_last_schedule_date(self, asset, row, extra_months): diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 9c9dd44971..d1b417fd38 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -105,7 +105,9 @@ class TestAssetRepair(unittest.TestCase): initial_num_of_depreciations = num_of_depreciations(asset) create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) asset.reload() + self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) + self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation) def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations @@ -126,7 +128,8 @@ def create_asset_repair(**args): "asset_name": asset.asset_name, "failure_date": nowdate(), "description": "Test Description", - "repair_cost": 0 + "repair_cost": 0, + "company": asset.company }) if args.stock_consumption: @@ -142,7 +145,7 @@ def create_asset_repair(**args): asset_repair.save() except frappe.DuplicateEntryError: pass - + if args.submit: asset_repair.repair_status = "Completed" asset_repair.cost_center = "_Test Cost Center - _TC" From 773aabae440a8d85d772ea0cd8f9749faee02dfc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Jun 2021 14:42:40 +0530 Subject: [PATCH 155/550] fix: allow to select group warehouse while downloading materials from production plan --- .../production_plan/production_plan.js | 21 ++++++- .../production_plan/production_plan.py | 59 ++++++++++--------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 64d584118f..056f600c3b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -306,8 +306,25 @@ frappe.ui.form.on('Production Plan', { }, download_materials_required: function(frm) { - let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); + const fields = [{ + fieldname: 'warehouses', + fieldtype: 'Table MultiSelect', + label: __('Warehouses'), + default: frm.doc.from_warehouse, + options: "Production Plan Material Request Warehouse", + get_query: function () { + return { + filters: { + company: frm.doc.company + } + }; + }, + }]; + + frappe.prompt(fields, (row) => { + let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses:row.warehouses }); + }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); }, show_progress: function(frm) { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 46e047654b..0ede1bd4ab 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -98,7 +98,7 @@ class ProductionPlan(Document): def get_items(self): self.set('po_items', []) if self.get_items_from == "Sales Order": - self.get_so_items() + self.get_so_items() elif self.get_items_from == "Material Request": self.get_mr_items() @@ -170,11 +170,11 @@ class ProductionPlan(Document): refs = {} for data in items: item_details = get_item_details(data.item_code) - if self.combine_items: + if self.combine_items: if item_details.bom_no in refs: refs[item_details.bom_no]['so_details'].append({ 'sales_order': data.parent, - 'sales_order_item': data.name, + 'sales_order_item': data.name, 'qty': data.pending_qty }) refs[item_details.bom_no]['qty'] += data.pending_qty @@ -188,10 +188,10 @@ class ProductionPlan(Document): } refs[item_details.bom_no]['so_details'].append({ 'sales_order': data.parent, - 'sales_order_item': data.name, + 'sales_order_item': data.name, 'qty': data.pending_qty }) - + pi = self.append('po_items', { 'include_exploded_items': 1, 'warehouse': data.warehouse, @@ -209,12 +209,12 @@ class ProductionPlan(Document): pi.sales_order = data.parent pi.sales_order_item = data.name pi.description = data.description - + elif self.get_items_from == "Material Request": pi.material_request = data.parent pi.material_request_item = data.name pi.description = data.description - + if refs: for po_item in self.po_items: po_item.planned_qty = refs[po_item.bom_no]['qty'] @@ -477,18 +477,19 @@ class ProductionPlan(Document): msgprint(_("No material request created")) @frappe.whitelist() -def download_raw_materials(doc): +def download_raw_materials(doc, warehouses=None): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', - 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', - 'Safety Stock', 'Required Qty']] + 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', + 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] - for d in get_items_for_material_requests(doc): + doc.warehouse = None + for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True): item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), - d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) + d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) if not doc.get('for_warehouse'): row = {'item_code': d.get('item_code')} @@ -507,7 +508,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, - item.purchase_uom, item_uom.conversion_factor + item.purchase_uom, item_uom.conversion_factor, item.safety_stock from `tabBOM Explosion Item` bei JOIN `tabBOM` bom ON bom.name = bei.parent @@ -677,32 +678,36 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, - ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` - where item_code = %(item_code)s {conditions} + ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, + ifnull(sum(planned_qty),0) as planned_qty + from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) +def get_warehouse_list(warehouses, warehouse_list=[]): + if isinstance(warehouses, string_types): + warehouses = json.loads(warehouses) + + for row in warehouses: + child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) + if child_warehouses: + warehouse_list.extend(child_warehouses) + else: + warehouse_list.append(row.get("warehouse")) + @frappe.whitelist() -def get_items_for_material_requests(doc, warehouses=None): +def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) warehouse_list = [] if warehouses: - if isinstance(warehouses, string_types): - warehouses = json.loads(warehouses) - - for row in warehouses: - child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) - if child_warehouses: - warehouse_list.extend(child_warehouses) - else: - warehouse_list.append(row.get("warehouse")) + get_warehouse_list(warehouses, warehouse_list) if warehouse_list: warehouses = list(set(warehouse_list)) - if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses: + if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: warehouses.remove(doc.get("for_warehouse")) warehouse_list = None @@ -795,7 +800,7 @@ def get_items_for_material_requests(doc, warehouses=None): if items: mr_items.append(items) - if not ignore_existing_ordered_qty and warehouses: + if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses: new_mr_items = [] for item in mr_items: get_materials_from_other_locations(item, warehouses, new_mr_items, company) From a34bf5edeceff190bf7c264cfa163443ea0aa71e Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 14:55:34 +0530 Subject: [PATCH 156/550] fix: Rename 'Stock Item' to 'Asset Repair Consumed Item' --- .../assets/doctype/asset_maintenance/asset_maintenance.json | 4 ++-- erpnext/assets/doctype/asset_repair/asset_repair.json | 4 ++-- .../{stock_item => asset_repair_consumed_item}/__init__.py | 0 .../asset_repair_consumed_item.json} | 2 +- .../asset_repair_consumed_item.py} | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename erpnext/assets/doctype/{stock_item => asset_repair_consumed_item}/__init__.py (100%) rename erpnext/assets/doctype/{stock_item/stock_item.json => asset_repair_consumed_item/asset_repair_consumed_item.json} (96%) rename erpnext/assets/doctype/{stock_item/stock_item.py => asset_repair_consumed_item/asset_repair_consumed_item.py} (81%) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index da2fd75451..63a55389d8 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -126,11 +126,11 @@ "fieldname": "stock_items", "fieldtype": "Table", "label": "Stock Items", - "options": "Stock Item" + "options": "Asset Repair Consumed Item" } ], "links": [], - "modified": "2021-05-13 05:24:58.480132", + "modified": "2021-06-21 14:53:46.041123", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index c25216c21b..6a14384f3f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -169,7 +169,7 @@ "fieldtype": "Table", "label": "Stock Items", "mandatory_depends_on": "stock_consumption", - "options": "Stock Item" + "options": "Asset Repair Consumed Item" }, { "fieldname": "section_break_23", @@ -264,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-20 17:35:51.075537", + "modified": "2021-06-21 14:53:46.665576", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/stock_item/__init__.py b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py similarity index 100% rename from erpnext/assets/doctype/stock_item/__init__.py rename to erpnext/assets/doctype/asset_repair_consumed_item/__init__.py diff --git a/erpnext/assets/doctype/stock_item/stock_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json similarity index 96% rename from erpnext/assets/doctype/stock_item/stock_item.json rename to erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index b1f05db395..528f0ec986 100644 --- a/erpnext/assets/doctype/stock_item/stock_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -46,7 +46,7 @@ "modified": "2021-05-12 03:19:55.006300", "modified_by": "Administrator", "module": "Assets", - "name": "Stock Item", + "name": "Asset Repair Consumed Item", "owner": "Administrator", "permissions": [], "sort_field": "modified", diff --git a/erpnext/assets/doctype/stock_item/stock_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py similarity index 81% rename from erpnext/assets/doctype/stock_item/stock_item.py rename to erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py index 0e3cc3f8ba..fa22a5712f 100644 --- a/erpnext/assets/doctype/stock_item/stock_item.py +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py @@ -4,5 +4,5 @@ # import frappe from frappe.model.document import Document -class StockItem(Document): +class AssetRepairConsumedItem(Document): pass From f88a13b292334475c6cc889eefafc2ccd556e987 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 15:02:40 +0530 Subject: [PATCH 157/550] fix(Asset Repair): Compute total_value instantly --- erpnext/assets/doctype/asset_repair/asset_repair.js | 7 +++++++ erpnext/assets/doctype/asset_repair/asset_repair.py | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index ced3dad1e5..91bed4fdd0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -35,6 +35,13 @@ frappe.ui.form.on('Asset Repair', { } }); +frappe.ui.form.on('Asset Repair Consumed Item', { + consumed_quantity: function(frm, cdt, cdn) { + var row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); + }, +}); + cur_frm.fields_dict.cost_center.get_query = function(doc) { return { filters: { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 5fccfb76a5..79b9a6a2b5 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -14,8 +14,6 @@ class AssetRepair(AccountsController): def validate(self): self.asset_doc = frappe.get_doc('Asset', self.asset) self.update_status() - if self.get('stock_items'): - self.set_total_value() # change later self.calculate_total_repair_cost() def update_status(self): @@ -24,10 +22,6 @@ class AssetRepair(AccountsController): else: self.asset_doc.set_status() - def set_total_value(self): - for item in self.get('stock_items'): - item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) - def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost if self.get('stock_items'): From 91a99e0d8973b97743065aa61f62635b13e05256 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 15:22:02 +0530 Subject: [PATCH 158/550] fix(Asset Repair): Increase stock quantity and decrease asset value on cancellation --- .../doctype/asset_repair/asset_repair.py | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 79b9a6a2b5..e7b8b45b7e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -24,9 +24,9 @@ class AssetRepair(AccountsController): def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost - if self.get('stock_items'): - for item in self.get('stock_items'): - self.total_repair_cost += item.total_value + + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + self.total_repair_cost += total_value_of_stock_consumed def on_submit(self): self.check_repair_status() @@ -44,6 +44,14 @@ class AssetRepair(AccountsController): self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.save() + def on_cancel(self): + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): + self.decrease_asset_value() + if self.get('stock_consumption'): + self.increase_stock_quantity() + if self.get('capitalize_repair_cost'): + self.make_gl_entries(cancel=True) + def check_repair_status(self): if self.repair_status == "Pending": frappe.throw(_("Please update Repair Status.")) @@ -55,10 +63,7 @@ class AssetRepair(AccountsController): frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse")) def increase_asset_value(self): - total_value_of_stock_consumed = 0 - if self.get('stock_consumption'): - for item in self.get('stock_items'): - total_value_of_stock_consumed += item.total_value + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() if self.asset_doc.calculate_depreciation: for row in self.asset_doc.finance_books: @@ -67,6 +72,24 @@ class AssetRepair(AccountsController): if self.capitalize_repair_cost: row.value_after_depreciation += self.repair_cost + def decrease_asset_value(self): + total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + + if self.asset_doc.calculate_depreciation: + for row in self.asset_doc.finance_books: + row.value_after_depreciation -= total_value_of_stock_consumed + + if self.capitalize_repair_cost: + row.value_after_depreciation -= self.repair_cost + + def get_total_value_of_stock_consumed(self): + total_value_of_stock_consumed = 0 + if self.get('stock_consumption'): + for item in self.get('stock_items'): + total_value_of_stock_consumed += item.total_value + + return total_value_of_stock_consumed + def decrease_stock_quantity(self): stock_entry = frappe.get_doc({ "doctype": "Stock Entry", @@ -86,8 +109,22 @@ class AssetRepair(AccountsController): self.stock_entry = stock_entry.name - def on_cancel(self): - self.make_gl_entries(cancel=True) + def increase_stock_quantity(self): + stock_entry = frappe.get_doc({ + "doctype": "Stock Entry", + "stock_entry_type": "Material Receipt", + "company": self.company + }) + + for stock_item in self.get('stock_items'): + stock_entry.append('items', { + "s_warehouse": self.warehouse, + "item_code": stock_item.item, + "qty": stock_item.consumed_quantity + }) + + stock_entry.insert() + stock_entry.submit() def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: From ffe306c2f603ac9eb1ff62792128dd55936271ca Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 16 Jun 2021 19:03:27 +0530 Subject: [PATCH 159/550] feat: details fetched from supplier group in supplier --- erpnext/buying/doctype/supplier/supplier.js | 13 +++++++++++++ erpnext/buying/doctype/supplier/supplier.py | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 4ddc458175..af6401b3fe 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Supplier Group Details'), function () { + frm.trigger("get_supplier_group_details"); + }, __('Actions')); + // indicators erpnext.utils.set_party_dashboard_indicators(frm); } }, + get_supplier_group_details: function(frm) { + frappe.call({ + method: "get_supplier_group_details", + doc: frm.doc, + callback: function(r){ + frm.refresh() + } + }); + }, is_internal_supplier: function(frm) { if (frm.doc.is_internal_supplier == 1) { diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index edeb135d95..791f71ed3b 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -51,6 +51,23 @@ class Supplier(TransactionBase): validate_party_accounts(self) self.validate_internal_supplier() + @frappe.whitelist() + def get_supplier_group_details(self): + doc = frappe.get_doc('Supplier Group', self.supplier_group) + self.payment_terms = "" + self.accounts = [] + + if not self.accounts and doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + self.save() + + if not self.payment_terms and doc.payment_terms: + self.payment_terms = doc.payment_terms + + def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") @@ -86,4 +103,4 @@ class Supplier(TransactionBase): create_contact(supplier, 'Supplier', doc.name, args.get('supplier_email_' + str(i))) except frappe.NameError: - pass \ No newline at end of file + pass From 89215e44a41654e522126976f25e12870aa8ad6b Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 17 Jun 2021 15:48:55 +0530 Subject: [PATCH 160/550] feat: details fetched from customer group in customer --- erpnext/selling/doctype/customer/customer.js | 17 ++++++++++++- erpnext/selling/doctype/customer/customer.py | 26 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 825b170a90..91944adef3 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -130,6 +130,10 @@ frappe.ui.form.on("Customer", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Customer Group Details'), function () { + frm.trigger("get_customer_group_details"); + }, __('Actions')); + // indicator erpnext.utils.set_party_dashboard_indicators(frm); @@ -145,4 +149,15 @@ frappe.ui.form.on("Customer", { if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name); }, -}); \ No newline at end of file + get_customer_group_details: function(frm) { + frappe.call({ + method: "get_customer_group_details", + doc: frm.doc, + callback: function(r){ + frm.refresh() + } + }); + + } +}); + diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 818888c0c1..cdeb089618 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -78,6 +78,32 @@ class Customer(TransactionBase): if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) + @frappe.whitelist() + def get_customer_group_details(self): + doc = frappe.get_doc('Customer Group', self.customer_group) + self.accounts = self.credit_limits = [] + self.payment_terms = self.default_price_list = "" + + if not self.accounts and doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + self.save() + + if not self.credit_limits and doc.credit_limits: + for credit in doc.credit_limits: + child = self.append('credit_limits') + child.company = credit.company + child.credit_limit = credit.credit_limit + self.save() + + if not self.payment_terms and doc.payment_terms: + self.payment_terms = doc.payment_terms + + if not self.default_price_list and doc.default_price_list: + self.default_price_list = doc.default_price_list + def check_customer_group_change(self): frappe.flags.customer_group_changed = False From 8347eb1dbae8591cb42fd4a2398c7fad0cdcf6fb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 21 Jun 2021 15:38:44 +0530 Subject: [PATCH 161/550] Update production_plan.js --- .../manufacturing/doctype/production_plan/production_plan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 056f600c3b..450aa04a73 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -323,7 +323,7 @@ frappe.ui.form.on('Production Plan', { frappe.prompt(fields, (row) => { let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses:row.warehouses }); + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses }); }, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock')); }, From acef77fb53972241becfbe5f19c6545d02aa8f79 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 18 Jun 2021 18:53:28 +0530 Subject: [PATCH 162/550] test: test case for fetching supplier group details --- .../buying/doctype/supplier/test_supplier.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index f9c8d35518..faa813aa4c 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -13,6 +13,26 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): + def test_get_supplier_group_details(self): + doc = frappe.get_doc("Supplier Group", "Local") + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + doc.append("accounts", test_account_details) + doc.save() + doc = frappe.get_doc("Supplier", "_Test Supplier") + doc.supplier_group = "Local" + doc.payment_terms = "" + doc.accounts = [] + doc.save() + doc.get_supplier_group_details() + self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(doc.accounts[0].company, "_Test Company") + self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date frappe.db.set_value( @@ -136,4 +156,4 @@ def create_supplier(**args): return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Supplier", args.supplier_name) \ No newline at end of file + return frappe.get_doc("Supplier", args.supplier_name) From 38f105eaee6a3f78d6167f7ce1c9da1660875cdc Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 18 Jun 2021 19:13:18 +0530 Subject: [PATCH 163/550] test: test cases for fetching customer group details --- .../selling/doctype/customer/test_customer.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7761aa70fb..8cb07aaa8a 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -27,6 +27,38 @@ class TestCustomer(unittest.TestCase): def tearDown(self): set_credit_limit('_Test Customer', '_Test Company', 0) + def test_get_customer_group_details(self): + doc = frappe.get_doc("Customer Group", "Commercial") + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + doc.default_price_list = "Standard Buying" + doc.credit_limits = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + test_credit_limits = { + "company": "_Test Company", + "credit_limit": 350000 + } + doc.append("accounts", test_account_details) + doc.append("credit_limits", test_credit_limits) + doc.save() + + doc = frappe.get_doc("Customer", "_Test Customer") + doc.customer_group = "Commercial" + doc.payment_terms = doc.default_price_list = "" + doc.accounts = doc.credit_limits= [] + doc.save() + doc.get_customer_group_details() + self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + + self.assertEqual(doc.accounts[0].company, "_Test Company") + self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + + self.assertEqual(doc.credit_limits[0].company, "_Test Company") + self.assertEqual(doc.credit_limits[0].credit_limit, 350000 ) + def test_party_details(self): from erpnext.accounts.party import get_party_details From 49ec0e5ac3be9333d6ee07980fb408d03e107de4 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 16:18:35 +0530 Subject: [PATCH 164/550] feat: Optionally allow rejected quality inspection on submission --- erpnext/controllers/stock_controller.py | 84 ++++++++++++------- .../stock_entry_detail.json | 3 +- .../stock_settings/stock_settings.json | 21 ++++- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 35097b97b9..3112fa7a6c 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -356,42 +356,68 @@ class StockController(AccountsController): }, update_modified) def validate_inspection(self): - '''Checks if quality inspection is set for Items that require inspection. - On submit, throw an exception''' - inspection_required_fieldname = None - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - inspection_required_fieldname = "inspection_required_before_purchase" - elif self.doctype in ["Delivery Note", "Sales Invoice"]: - inspection_required_fieldname = "inspection_required_before_delivery" + """Checks if quality inspection is set/ is valid for Items that require inspection.""" + inspection_fieldname_map = { + "Purchase Receipt": "inspection_required_before_purchase", + "Purchase Invoice": "inspection_required_before_purchase", + "Sales Invoice": "inspection_required_before_delivery", + "Delivery Note": "inspection_required_before_delivery" + } + inspection_required_fieldname = inspection_fieldname_map.get(self.doctype) + # return if inspection is not required on document level if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or (self.doctype == "Stock Entry" and not self.inspection_required) or (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): return - for d in self.get('items'): - qa_required = False - if (inspection_required_fieldname and not d.quality_inspection and - frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): - qa_required = True - elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: - qa_required = True - if self.docstatus == 1 and d.quality_inspection: - qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) - if qa_doc.docstatus == 0: - link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) - frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) + for row in self.get('items'): + qi_required = False + if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)): + qi_required = True + elif self.doctype == "Stock Entry" and row.t_warehouse: + qi_required = True # inward stock needs inspection - if qa_doc.status != 'Accepted': - frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") - .format(d.idx, d.item_code), QualityInspectionRejectedError) - elif qa_required : - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted - if self.docstatus==1 and action == 'Stop': - frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), - exc=QualityInspectionRequiredError) - else: - frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) + if qi_required: # validate row only if inspection is required on item level + self.validate_qi_presence(row) + if self.docstatus == 1: + self.validate_qi_submission(row) + self.validate_qi_rejection(row) + + def validate_qi_presence(self, row): + """Check if QI is present on row level. Warn on save and stop on submit if missing.""" + if not row.quality_inspection: + msg = _(f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}") + if self.docstatus == 1: + frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError) + else: + frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue") + + def validate_qi_submission(self, row): + """Check if QI is submitted on row level, during submission""" + action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted or "Stop" + qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") + + if not qa_docstatus == 1: + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = _(f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}") + if action == "Stop": + frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + else: + frappe.msgprint(msg, alert=True) + + def validate_qi_rejection(self, row): + """Check if QI is rejected on row level, during submission""" + action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_rejected or "Stop" + qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") + + if qa_status == "Rejected": + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = _(f"Row #{row.idx}: Quality Inspection was rejected for item {row.item_code}") + if action == "Stop": + frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + else: + frappe.msgprint(msg, alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 864ff488b2..a007389f7a 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -307,6 +307,7 @@ "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", + "no_copy": 1, "options": "Quality Inspection" }, { @@ -548,7 +549,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-11 13:47:50.158754", + "modified": "2021-06-21 16:03:18.834880", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index cf5d98d092..d07e26b536 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -23,7 +23,10 @@ "allow_negative_stock", "show_barcode_field", "clean_description_html", + "quality_inspection_settings_section", "action_if_quality_inspection_is_not_submitted", + "column_break_21", + "action_if_quality_inspection_is_rejected", "section_break_7", "automatically_set_serial_nos_based_on_fifo", "set_qty_in_transactions_based_on_serial_no_input", @@ -264,6 +267,22 @@ { "fieldname": "column_break_31", "fieldtype": "Column Break" + }, + { + "fieldname": "quality_inspection_settings_section", + "fieldtype": "Section Break", + "label": "Quality Inspection Settings" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "default": "Stop", + "fieldname": "action_if_quality_inspection_is_rejected", + "fieldtype": "Select", + "label": "Action If Quality Inspection Is Rejected", + "options": "Stop\nWarn" } ], "icon": "icon-cog", @@ -271,7 +290,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-30 17:27:42.709231", + "modified": "2021-06-21 16:17:42.159829", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From ea0dea46e0c6126da7d304f5e3d6c9dae552fc75 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 16:51:12 +0530 Subject: [PATCH 165/550] fix: sider and semgrep --- erpnext/controllers/stock_controller.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 3112fa7a6c..9bac27deb1 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -387,11 +387,11 @@ class StockController(AccountsController): def validate_qi_presence(self, row): """Check if QI is present on row level. Warn on save and stop on submit if missing.""" if not row.quality_inspection: - msg = _(f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}") + msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" if self.docstatus == 1: - frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError) + frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) else: - frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue") + frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" @@ -400,11 +400,11 @@ class StockController(AccountsController): if not qa_docstatus == 1: link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) - msg = _(f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}") + msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" if action == "Stop": - frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: - frappe.msgprint(msg, alert=True) + frappe.msgprint(_(msg), alert=True) def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" @@ -413,11 +413,11 @@ class StockController(AccountsController): if qa_status == "Rejected": link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) - msg = _(f"Row #{row.idx}: Quality Inspection was rejected for item {row.item_code}") + msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" if action == "Stop": - frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) else: - frappe.msgprint(msg, alert=True, indicator="orange") + frappe.msgprint(_(msg), alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) From 5776a962b3d12f25f39c88208e8e10c9897830fd Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 18:57:11 +0530 Subject: [PATCH 166/550] fix(Asset Repair): Return Depreciation Schedule to original state on cancellation --- .../doctype/asset_repair/asset_repair.py | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index e7b8b45b7e..01b36880be 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -42,15 +42,25 @@ class AssetRepair(AccountsController): self.modify_depreciation_schedule() self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() self.asset_doc.save() def on_cancel(self): + self.asset_doc = frappe.get_doc('Asset', self.asset) + if self.get('stock_consumption') or self.get('capitalize_repair_cost'): self.decrease_asset_value() if self.get('stock_consumption'): self.increase_stock_quantity() if self.get('capitalize_repair_cost'): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.make_gl_entries(cancel=True) + if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life: + self.revert_depreciation_schedule_on_cancellation() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() def check_repair_status(self): if self.repair_status == "Pending": @@ -101,7 +111,8 @@ class AssetRepair(AccountsController): stock_entry.append('items', { "s_warehouse": self.warehouse, "item_code": stock_item.item, - "qty": stock_item.consumed_quantity + "qty": stock_item.consumed_quantity, + "basic_rate": stock_item.valuation_rate }) stock_entry.insert() @@ -118,7 +129,7 @@ class AssetRepair(AccountsController): for stock_item in self.get('stock_items'): stock_entry.append('items', { - "s_warehouse": self.warehouse, + "t_warehouse": self.warehouse, "item_code": stock_item.item, "qty": stock_item.consumed_quantity }) @@ -126,6 +137,8 @@ class AssetRepair(AccountsController): stock_entry.insert() stock_entry.submit() + self.stock_entry = stock_entry.name + def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: gl_entries = self.get_gl_entries() @@ -216,6 +229,34 @@ class AssetRepair(AccountsController): if asset.to_date > schedule_date: row.total_number_of_depreciations += 1 + def revert_depreciation_schedule_on_cancellation(self): + for row in self.asset_doc.finance_books: + row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation + + self.asset_doc.flags.increase_in_asset_life = False + extra_months = self.increase_in_asset_life % row.frequency_of_depreciation + if extra_months != 0: + self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months) + + def calculate_last_schedule_date_before_modification(self, asset, row, extra_months): + asset.flags.increase_in_asset_life = True + number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \ + cint(asset.number_of_depreciations_booked) + + # the Schedule Date in the final row of the modified Depreciation Schedule + last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date + + # the Schedule Date in the final row of the original Depreciation Schedule + asset.to_date = add_months(last_schedule_date, -extra_months) + + # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations + # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... + schedule_date = add_months(row.depreciation_start_date, + (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation)) + + if asset.to_date < schedule_date: + row.total_number_of_depreciations -= 1 + @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) From f97206b3cbb9aeee730d67db8161f75138404595 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 19:38:37 +0530 Subject: [PATCH 167/550] fix: Sort website products by weightage mentioned in Item master --- erpnext/shopping_cart/product_query.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 36d446ed0f..dd94c26bc6 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -61,7 +61,8 @@ class ProductQuery: ], or_filters=self.or_filters, start=start, - limit=self.page_length + limit=self.page_length, + order_by="weightage desc" ) items_dict = {item.name: item for item in items} @@ -71,7 +72,15 @@ class ProductQuery: result = [items_dict.get(item) for item in list(set.intersection(*all_items))] else: - result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length) + result = frappe.get_all( + "Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + start=start, + limit=self.page_length, + order_by="weightage desc" + ) for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') From 600333875a1e275d799988c35a9d0ccb80893737 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 23:41:35 +0530 Subject: [PATCH 168/550] fix(Sales Invoice): Let item.asset be copied on duplicating the doc --- .../doctype/sales_invoice_item/sales_invoice_item.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 8e6952a93c..6690bdafc3 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -743,7 +743,6 @@ "fieldname": "asset", "fieldtype": "Link", "label": "Asset", - "no_copy": 1, "options": "Asset" }, { @@ -826,7 +825,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-02-23 01:05:22.123527", + "modified": "2021-06-21 23:03:11.599901", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", From 5b07e58412ae3b86c67fa90833b603b5dac4b64f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 21 Jun 2021 23:51:09 +0530 Subject: [PATCH 169/550] fix(Sales Invoice): Let invoice be created for Sold Assets if it's a return invoice --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a008742390..c835debce6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -149,7 +149,7 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled", "Sold"): + elif asset.status in ("Scrapped", "Cancelled") or asset.status == "Sold" and not self.is_return: frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) def validate_item_cost_centers(self): From d0d5fedd484339038064f803460978eaa38196b1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 00:22:08 +0530 Subject: [PATCH 170/550] fix(Sales Invoice): Print appropriate message if item.asset is missing when the Item is a Fixed Asset --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c835debce6..ff1e14b30a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -916,7 +916,11 @@ class SalesInvoice(SellingController): for item in self.get("items"): if flt(item.base_net_amount, item.precision("base_net_amount")): if item.is_fixed_asset: - asset = frappe.get_doc("Asset", item.asset) + if item.get('asset'): + asset = frappe.get_doc("Asset", item.asset) + else: + frappe.throw(_("Enter Asset linked with Item {0}: {1} in row {2}.") + .format(item.item_code, item.item_name, item.idx)) if (len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book): From 703c30f5f89183937b2dcdecf33089a993e98a82 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 22 Jun 2021 11:20:17 +0530 Subject: [PATCH 171/550] fix: Consistent alert indicators --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 9bac27deb1..c83de3da9e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -404,7 +404,7 @@ class StockController(AccountsController): if action == "Stop": frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: - frappe.msgprint(_(msg), alert=True) + frappe.msgprint(_(msg), alert=True, indicator="orange") def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" From cfdda21dd2a353cafbe7d2d349ad06714d6061dd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 13:03:22 +0530 Subject: [PATCH 172/550] fix: Export invoices not visible in GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 80e2d725a2..10961593e1 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -201,7 +201,7 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ - conditions += " AND billing_address_gstin NOT IN %(company_gstins)s" + conditions += " AND IFNULL(billing_address_gstin, '') NOT IN %(company_gstins)s" return conditions From 3b0eac79bf6b302d2b4800c2f6333828f00d4bff Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 16:26:09 +0530 Subject: [PATCH 173/550] fix(Asset Repair): Compute total_value --- erpnext/assets/doctype/asset_repair/asset_repair.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 01b36880be..342a8861e6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -14,6 +14,9 @@ class AssetRepair(AccountsController): def validate(self): self.asset_doc = frappe.get_doc('Asset', self.asset) self.update_status() + + if self.get('stock_items'): + self.set_total_value() self.calculate_total_repair_cost() def update_status(self): @@ -22,6 +25,10 @@ class AssetRepair(AccountsController): else: self.asset_doc.set_status() + def set_total_value(self): + for item in self.get('stock_items'): + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost From 889140fd8c5c8684f394a36516878a2490efe21c Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:26:19 +0530 Subject: [PATCH 174/550] fix: sql syntax error in get_project_name method (#26147) --- erpnext/controllers/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 7bd739a6ad..4ada834425 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select {fields} from `tabProject` where `tabProject`.status not in ("Completed", "Cancelled") - and {cond} {match_cond} {scond} + and {cond} {scond} {match_cond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), idx desc, From 3a44d888661b0d3b81e475307484dc09ba4b2c78 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 16:28:29 +0530 Subject: [PATCH 175/550] fix(Asset): Fix tests for Asset Repair --- erpnext/assets/doctype/asset/test_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 29fbc9f15d..f3667c7b95 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -707,7 +707,7 @@ def create_asset(**args): "available_for_use_date": "2020-06-06", "location": "Test Location", "asset_owner": "Company", - "is_existing_asset": args.is_existing_asset or 0 + "is_existing_asset": 1 }) if asset.calculate_depreciation: From 219a87d53072cab01a79009f1195a359ce059a4a Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 22 Jun 2021 16:28:58 +0530 Subject: [PATCH 176/550] fix: disable sales order cancellation if linked to draft invoice (#26125) --- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- .../selling/doctype/sales_order/test_sales_order.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 551f715bd5..41f57a34d3 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -233,7 +233,7 @@ class SalesOrder(SellingController): # Checks Sales Invoice submit_rv = frappe.db.sql_list("""select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 - where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", + where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""", self.name) if submit_rv: diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 987371066a..974648d6d4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1217,6 +1217,19 @@ class TestSalesOrder(unittest.TestCase): # To test if the SO does NOT have a Blanket Order self.assertEqual(so_doc.items[0].blanket_order, None) + def test_so_cancellation_when_si_drafted(self): + """ + Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state + Expected result: sales order should not get cancelled + """ + so = make_sales_order() + so.submit() + si = make_sales_invoice(so.name) + si.save() + + self.assertRaises(frappe.ValidationError, so.cancel) + + def make_sales_order(**args): so = frappe.new_doc("Sales Order") From 2ea325c0c3fbdc97e2aa1427f565ee176f543a4c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 16:33:10 +0530 Subject: [PATCH 177/550] fix(Asset Repair): Revert Stock Entry on cancellation --- .../doctype/asset_repair/asset_repair.py | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 342a8861e6..4261c535af 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -125,26 +125,11 @@ class AssetRepair(AccountsController): stock_entry.insert() stock_entry.submit() - self.stock_entry = stock_entry.name + self.db_set('stock_entry', stock_entry.name) def increase_stock_quantity(self): - stock_entry = frappe.get_doc({ - "doctype": "Stock Entry", - "stock_entry_type": "Material Receipt", - "company": self.company - }) - - for stock_item in self.get('stock_items'): - stock_entry.append('items', { - "t_warehouse": self.warehouse, - "item_code": stock_item.item, - "qty": stock_item.consumed_quantity - }) - - stock_entry.insert() - stock_entry.submit() - - self.stock_entry = stock_entry.name + stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + stock_entry.cancel() def make_gl_entries(self, cancel=False): if flt(self.repair_cost) > 0: From 859c8c92f7a73f4b85fd7bea244169e69fda3b24 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 17:16:12 +0530 Subject: [PATCH 178/550] fix: Remove changes made to Asset Maintenance --- .../asset_maintenance/asset_maintenance.js | 3 -- .../asset_maintenance/asset_maintenance.json | 31 +--------------- .../asset_maintenance/asset_maintenance.py | 37 +------------------ 3 files changed, 3 insertions(+), 68 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 19393b7e9d..70b8654509 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -30,10 +30,7 @@ frappe.ui.form.on('Asset Maintenance', { if(!frm.is_new()) { frm.trigger('make_dashboard'); } - - frm.toggle_display(['stock_consumption_details_section'], frm.doc.stock_consumption); }, - make_dashboard: (frm) => { if(!frm.is_new()) { frappe.call({ diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json index 63a55389d8..c0c2566fe2 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.json @@ -12,17 +12,13 @@ "column_break_3", "item_code", "item_name", - "stock_consumption", "section_break_6", "maintenance_team", "column_break_9", "maintenance_manager", "maintenance_manager_name", "section_break_8", - "asset_maintenance_tasks", - "stock_consumption_details_section", - "warehouse", - "stock_items" + "asset_maintenance_tasks" ], "fields": [ { @@ -104,33 +100,10 @@ "label": "Maintenance Tasks", "options": "Asset Maintenance Task", "reqd": 1 - }, - { - "default": "0", - "fieldname": "stock_consumption", - "fieldtype": "Check", - "label": "Stock Consumed During Maintenance" - }, - { - "fieldname": "stock_consumption_details_section", - "fieldtype": "Section Break", - "label": "Stock Consumption Details" - }, - { - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "options": "Warehouse" - }, - { - "fieldname": "stock_items", - "fieldtype": "Table", - "label": "Stock Items", - "options": "Asset Repair Consumed Item" } ], "links": [], - "modified": "2021-06-21 14:53:46.041123", + "modified": "2020-05-28 20:28:32.993823", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance", diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index e3e654c398..a506deec93 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -19,45 +19,10 @@ class AssetMaintenance(Document): if not task.assign_to and self.docstatus == 0: throw(_("Row #{}: Please asign task to a member.").format(task.idx)) - if self.stock_consumption: - self.check_for_stock_items_and_warehouse() - self.increase_asset_value() - self.decrease_stock_quantity() - def on_update(self): for task in self.get('asset_maintenance_tasks'): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) - self.sync_maintenance_tasks() - - def check_for_stock_items_and_warehouse(self): - if self.stock_consumption: - if not self.stock_items: - frappe.throw(_("Please enter Stock Items consumed during Asset Maintenance.")) - if not self.warehouse: - frappe.throw(_("Please enter Warehouse from which Stock Items consumed during Asset Maintenance were taken.")) - - def increase_asset_value(self): - asset_value = frappe.db.get_value('Asset', self.asset_name, 'asset_value') - for item in self.stock_items: - asset_value += item.total_value - - frappe.db.set_value('Asset', self.asset_name, 'asset_value', asset_value) - - def decrease_stock_quantity(self): - stock_entry = frappe.get_doc({ - "doctype": "Stock Entry", - "stock_entry_type": "Material Issue" - }) - - for stock_item in self.stock_items: - stock_entry.append('items', { - "s_warehouse": self.warehouse, - "item_code": stock_item.item, - "qty": stock_item.consumed_quantity - }) - - stock_entry.insert() - stock_entry.submit() + self.sync_maintenance_tasks() def sync_maintenance_tasks(self): tasks_names = [] From 2e4596540516a2f053f47e0902060b7e235b5d42 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 17:24:14 +0530 Subject: [PATCH 179/550] fix(Asset Repair): Fix Sider issues --- erpnext/assets/doctype/asset_repair/asset_repair.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 4261c535af..6054258ea6 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -16,7 +16,7 @@ class AssetRepair(AccountsController): self.update_status() if self.get('stock_items'): - self.set_total_value() + self.set_total_value() self.calculate_total_repair_cost() def update_status(self): @@ -26,8 +26,8 @@ class AssetRepair(AccountsController): self.asset_doc.set_status() def set_total_value(self): - for item in self.get('stock_items'): - item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + for item in self.get('stock_items'): + item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) def calculate_total_repair_cost(self): self.total_repair_cost = self.repair_cost From c05496a5a7c52e7f7c4a8a4f5c55f71b52d810a8 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 22 Jun 2021 17:53:53 +0530 Subject: [PATCH 180/550] fix: fixed rounding off ordered percent to 100 in condition (#26152) --- erpnext/stock/doctype/material_request/material_request.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 92c8d21387..6e66f9869c 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -101,7 +101,8 @@ frappe.ui.form.on('Material Request', { } if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { - if (flt(frm.doc.per_ordered, 2) < 100) { + let precision = frappe.defaults.get_default("float_precision"); + if (flt(frm.doc.per_ordered, precision) < 100) { let add_create_pick_list_button = () => { frm.add_custom_button(__('Pick List'), () => frm.events.create_pick_list(frm), __('Create')); From ea2c02738d55d17c2d8161bdae5ce6454f7d4fa9 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 22 Jun 2021 21:35:25 +0530 Subject: [PATCH 181/550] fix: Include Stock Reco logic in update_qty_in_future_sle --- erpnext/stock/stock_ledger.py | 75 ++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9fe89c3fa5..94bd3077a7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -215,7 +215,7 @@ class update_entries_after(object): """ self.data.setdefault(args.warehouse, frappe._dict()) warehouse_dict = self.data[args.warehouse] - previous_sle = self.get_previous_sle_of_current_voucher(args) + previous_sle = get_previous_sle_of_current_voucher(args) warehouse_dict.previous_sle = previous_sle for key in ("qty_after_transaction", "valuation_rate", "stock_value"): @@ -227,29 +227,6 @@ class update_entries_after(object): "stock_value_difference": 0.0 }) - def get_previous_sle_of_current_voucher(self, args): - """get stock ledger entries filtered by specific posting datetime conditions""" - - args['time_format'] = '%H:%i:%s' - if not args.get("posting_date"): - args["posting_date"] = "1900-01-01" - if not args.get("posting_time"): - args["posting_time"] = "00:00" - - sle = frappe.db.sql(""" - select *, timestamp(posting_date, posting_time) as "timestamp" - from `tabStock Ledger Entry` - where item_code = %(item_code)s - and warehouse = %(warehouse)s - and is_cancelled = 0 - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) - order by timestamp(posting_date, posting_time) desc, creation desc - limit 1 - for update""", args, as_dict=1) - - return sle[0] if sle else frappe._dict() - - def build(self): from erpnext.controllers.stock_controller import future_sle_exists @@ -734,6 +711,35 @@ class update_entries_after(object): bin_doc.flags.via_stock_ledger_entry = True bin_doc.save(ignore_permissions=True) + +def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): + """get stock ledger entries filtered by specific posting datetime conditions""" + + args['time_format'] = '%H:%i:%s' + if not args.get("posting_date"): + args["posting_date"] = "1900-01-01" + if not args.get("posting_time"): + args["posting_time"] = "00:00" + + voucher_condition = "" + if exclude_current_voucher: + voucher_no = args.get("voucher_no") + voucher_condition = f"and voucher_no != '{voucher_no}'" + + sle = frappe.db.sql(""" + select *, timestamp(posting_date, posting_time) as "timestamp" + from `tabStock Ledger Entry` + where item_code = %(item_code)s + and warehouse = %(warehouse)s + and is_cancelled = 0 + {voucher_condition} + and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + order by timestamp(posting_date, posting_time) desc, creation desc + limit 1 + for update""".format(voucher_condition=voucher_condition), args, as_dict=1) + + return sle[0] if sle else frappe._dict() + def get_previous_sle(args, for_update=False): """ get the last sle on or before the current time-bucket, @@ -862,9 +868,24 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate def update_qty_in_future_sle(args, allow_negative_stock=None): + """Recalculate Qty after Transaction in future SLEs based on current SLE.""" + qty_shift = args.actual_qty + + # find difference/shift in qty caused by stock reconciliation + if args.voucher_type == "Stock Reconciliation": + last_balance = get_previous_sle_of_current_voucher( + args, + exclude_current_voucher=True + ).get("qty_after_transaction") + if last_balance is not None: + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = args.qty_after_transaction + qty_shift = stock_reco_qty_shift + frappe.db.sql(""" update `tabStock Ledger Entry` - set qty_after_transaction = qty_after_transaction + {qty} + set qty_after_transaction = qty_after_transaction + {qty_shift} where item_code = %(item_code)s and warehouse = %(warehouse)s @@ -876,7 +897,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): and creation > %(creation)s ) ) - """.format(qty=args.actual_qty), args) + """.format(qty_shift=qty_shift), args) validate_negative_qty_in_future_sle(args, allow_negative_stock) @@ -884,7 +905,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): allow_negative_stock = allow_negative_stock \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if args.actual_qty < 0 and not allow_negative_stock: + if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: sle = get_future_sle_with_negative_qty(args) if sle: message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( From 8127208774d1db413c3f86b0f728420122fdd419 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 22 Jun 2021 23:12:56 +0530 Subject: [PATCH 182/550] fix(Sales Invoice): Reset Asset status on issuing Credit Note --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4655ea8222..58a2a33473 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -936,7 +936,8 @@ class SalesInvoice(SellingController): gl_entries.append(self.get_gl_dict(gle, item=item)) asset.db_set("disposal_date", self.posting_date) - asset.set_status("Sold" if self.docstatus==1 else None) + self.set_asset_status(asset) + else: # Do not book income for transfer within same company if not self.is_internal_transfer(): @@ -962,6 +963,12 @@ class SalesInvoice(SellingController): erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() + def set_asset_status(self, asset): + if self.is_return: + asset.set_status() + else: + asset.set_status("Sold" if self.docstatus==1 else None) + def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points): gl_entries.append( From fc98abece9b6c1975b228e8b37b8921d45a3bfac Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 23 Jun 2021 11:21:38 +0530 Subject: [PATCH 183/550] feat: Employee Grievance (#25705) * feat: Employee Grievance * feat: link to desk and automatic unsuspend * test: Employee Grievance * fix: Sider and Translation * fix: sider * fix: formatting * feat: changes requested * feat: Employee Grievance * feat: link to desk and automatic unsuspend * test: Employee Grievance * fix: Sider and Translation * fix: sider * fix: formatting * feat: changes requested * fix: patch test and sider issue * fix: make Employee Responsible non-mandatory since there cannot be an employee responsible for all sorts of grievances - show pay cut and suspension buttons only if Employee Resposible is set - some label changes * feat: added subject field for more context - set title for documents - added list view settings - refactor suspend and unsuspend functions - add submit and cancel perms for system and hr managers - fix tests * fix: sider issues * fix: removed suspension and paycut * fix:sider * fix: test * fix: test * fix: resolved Conflicts * fix: sider * fix: remove debugging print statements * fix: validation message * fix: unnecessary comma Co-authored-by: Rucha Mahabal --- erpnext/controllers/queries.py | 2 +- erpnext/hr/doctype/employee/employee.json | 4 +- erpnext/hr/doctype/employee/employee.py | 5 +- .../hr/doctype/employee/employee_dashboard.py | 5 +- erpnext/hr/doctype/employee/employee_list.js | 2 +- .../hr/doctype/employee_grievance/__init__.py | 0 .../employee_grievance/employee_grievance.js | 39 +++ .../employee_grievance.json | 261 ++++++++++++++++++ .../employee_grievance/employee_grievance.py | 15 + .../employee_grievance_list.js | 12 + .../test_employee_grievance.py | 51 ++++ erpnext/hr/doctype/grievance_type/__init__.py | 0 .../doctype/grievance_type/grievance_type.js | 8 + .../grievance_type/grievance_type.json | 70 +++++ .../doctype/grievance_type/grievance_type.py | 8 + .../grievance_type/test_grievance_type.py | 8 + erpnext/hr/workspace/hr/hr.json | 20 +- .../additional_salary/additional_salary.js | 17 +- .../doctype/salary_slip/test_salary_slip.py | 1 + .../salary_structure/test_salary_structure.py | 4 +- 20 files changed, 516 insertions(+), 16 deletions(-) create mode 100644 erpnext/hr/doctype/employee_grievance/__init__.py create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.js create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.json create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance.py create mode 100644 erpnext/hr/doctype/employee_grievance/employee_grievance_list.js create mode 100644 erpnext/hr/doctype/employee_grievance/test_employee_grievance.py create mode 100644 erpnext/hr/doctype/grievance_type/__init__.py create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.js create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.json create mode 100644 erpnext/hr/doctype/grievance_type/grievance_type.py create mode 100644 erpnext/hr/doctype/grievance_type/test_grievance_type.py diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 4ada834425..280319321f 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Employee", ["name", "employee_name"]) return frappe.db.sql("""select {fields} from `tabEmployee` - where status = 'Active' + where status in ('Active', 'Suspended') and docstatus < 2 and ({key} like %(txt)s or employee_name like %(txt)s) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 5442ed56c3..d592a9c79e 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -207,7 +207,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Active\nInactive\nLeft", + "options": "Active\nInactive\nSuspended\nLeft", "reqd": 1, "search_index": 1 }, @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2021-06-12 11:31:37.730760", + "modified": "2021-06-17 11:31:37.730760", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index bc5694226a..fa017d9d4c 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr +from frappe.utils import getdate, validate_email_address, today, add_years, cstr from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ @@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet -from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass @@ -37,7 +36,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Active", "Inactive", "Left"]) + validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"]) self.employee = self.name self.set_employee_name() diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 285374d9f6..e853bee69f 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -7,7 +7,8 @@ def get_data(): 'heatmap_message': _('This is based on the attendance of this Employee'), 'fieldname': 'employee', 'non_standard_fieldnames': { - 'Bank Account': 'party' + 'Bank Account': 'party', + 'Employee Grievance': 'raised_by' }, 'transactions': [ { @@ -20,7 +21,7 @@ def get_data(): }, { 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] + 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] }, { 'label': _('Shift'), diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js index 6679e318c2..d37e1496ca 100644 --- a/erpnext/hr/doctype/employee/employee_list.js +++ b/erpnext/hr/doctype/employee/employee_list.js @@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = { filters: [["status","=", "Active"]], get_indicator: function(doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; + indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status]; return indicator; } }; diff --git a/erpnext/hr/doctype/employee_grievance/__init__.py b/erpnext/hr/doctype/employee_grievance/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.js b/erpnext/hr/doctype/employee_grievance/employee_grievance.js new file mode 100644 index 0000000000..25c5badbc7 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.js @@ -0,0 +1,39 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Grievance', { + setup: function(frm) { + frm.set_query('grievance_against_party', function() { + return { + filters: { + name: ['in', [ + 'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee'] + ] + } + }; + }); + frm.set_query('associated_document_type', function() { + let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website", + "Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"]; + return { + filters: { + istable: 0, + issingle: 0, + module: ["Not In", ignore_modules] + } + }; + }); + }, + + grievance_against_party: function(frm) { + let filters = {}; + if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) { + filters.name = ["!=", frm.doc.raised_by]; + } + frm.set_query('grievance_against', function() { + return { + filters: filters + }; + }); + }, +}); diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.json b/erpnext/hr/doctype/employee_grievance/employee_grievance.json new file mode 100644 index 0000000000..5a918562af --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.json @@ -0,0 +1,261 @@ +{ + "actions": [], + "autoname": "HR-GRIEV-.YYYY.-.#####", + "creation": "2021-05-11 13:41:51.485295", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "subject", + "raised_by", + "employee_name", + "designation", + "column_break_3", + "date", + "status", + "reports_to", + "grievance_details_section", + "grievance_against_party", + "grievance_against", + "grievance_type", + "column_break_11", + "associated_document_type", + "associated_document", + "section_break_14", + "description", + "investigation_details_section", + "cause_of_grievance", + "resolution_details_section", + "resolved_by", + "resolution_date", + "employee_responsible", + "column_break_16", + "resolution_detail", + "amended_from" + ], + "fields": [ + { + "fieldname": "grievance_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Type", + "options": "Grievance Type", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date ", + "reqd": 1 + }, + { + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Open\nInvestigated\nResolved\nInvalid", + "reqd": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "cause_of_grievance", + "fieldtype": "Text", + "label": "Cause of Grievance", + "mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\"" + }, + { + "fieldname": "resolution_details_section", + "fieldtype": "Section Break", + "label": "Resolution Details" + }, + { + "fieldname": "resolved_by", + "fieldtype": "Link", + "label": "Resolved By", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"", + "options": "User" + }, + { + "fieldname": "employee_responsible", + "fieldtype": "Link", + "label": "Employee Responsible ", + "options": "Employee" + }, + { + "fieldname": "resolution_detail", + "fieldtype": "Small Text", + "label": "Resolution Details", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "resolution_date", + "fieldtype": "Date", + "label": "Resolution Date", + "mandatory_depends_on": "eval: doc.status == \"Resolved\"" + }, + { + "fieldname": "grievance_against", + "fieldtype": "Dynamic Link", + "label": "Grievance Against", + "options": "grievance_against_party", + "reqd": 1 + }, + { + "fieldname": "raised_by", + "fieldtype": "Link", + "label": "Raised By", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Grievance", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "raised_by.designation", + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation", + "read_only": 1 + }, + { + "fetch_from": "raised_by.reports_to", + "fieldname": "reports_to", + "fieldtype": "Link", + "label": "Reports To", + "options": "Employee", + "read_only": 1 + }, + { + "fieldname": "grievance_details_section", + "fieldtype": "Section Break", + "label": "Grievance Details" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Section Break" + }, + { + "fieldname": "grievance_against_party", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Grievance Against Party", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "associated_document_type", + "fieldtype": "Link", + "label": "Associated Document Type", + "options": "DocType" + }, + { + "fieldname": "associated_document", + "fieldtype": "Dynamic Link", + "label": "Associated Document", + "options": "associated_document_type" + }, + { + "fieldname": "investigation_details_section", + "fieldtype": "Section Break", + "label": "Investigation Details" + }, + { + "fetch_from": "raised_by.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-06-21 12:51:01.499486", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Grievance", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "search_fields": "subject,raised_by,grievance_against_party", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "subject", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py new file mode 100644 index 0000000000..503b5ea444 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, bold +from frappe.model.document import Document + +class EmployeeGrievance(Document): + def on_submit(self): + if self.status not in ["Invalid", "Resolved"]: + frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format( + bold("Invalid"), + bold("Resolved")) + ) + diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js new file mode 100644 index 0000000000..fc08e21609 --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Employee Grievance"] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + var colors = { + "Open": "red", + "Investigated": "orange", + "Resolved": "green", + "Invalid": "grey" + }; + return [__(doc.status), colors[doc.status], "status,=," + doc.status]; + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py new file mode 100644 index 0000000000..a615b20a5a --- /dev/null +++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py @@ -0,0 +1,51 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +import unittest +from frappe.utils import today +from erpnext.hr.doctype.employee.test_employee import make_employee +class TestEmployeeGrievance(unittest.TestCase): + def test_create_employee_grievance(self): + create_employee_grievance() + +def create_employee_grievance(): + grievance_type = create_grievance_type() + emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company") + emp_2 = make_employee("testculprit@example.com", company="_Test Company") + + grievance = frappe.new_doc("Employee Grievance") + grievance.subject = "Test Employee Grievance" + grievance.raised_by = emp_1 + grievance.date = today() + grievance.grievance_type = grievance_type + grievance.grievance_against_party = "Employee" + grievance.grievance_against = emp_2 + grievance.description = "test descrip" + + #set cause + grievance.cause_of_grievance = "test cause" + + #resolution details + grievance.resolution_date = today() + grievance.resolution_detail = "test resolution detail" + grievance.resolved_by = "test_emp_grievance_@example.com" + grievance.employee_responsible = emp_2 + grievance.status = "Resolved" + + grievance.save() + grievance.submit() + + return grievance + + +def create_grievance_type(): + if frappe.db.exists("Grievance Type", "Employee Abuse"): + return frappe.get_doc("Grievance Type", "Employee Abuse") + grievance_type = frappe.new_doc("Grievance Type") + grievance_type.name = "Employee Abuse" + grievance_type.description = "Test" + grievance_type.save() + + return grievance_type.name + diff --git a/erpnext/hr/doctype/grievance_type/__init__.py b/erpnext/hr/doctype/grievance_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.js b/erpnext/hr/doctype/grievance_type/grievance_type.js new file mode 100644 index 0000000000..425f2fd5b5 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Grievance Type', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.json b/erpnext/hr/doctype/grievance_type/grievance_type.json new file mode 100644 index 0000000000..1dce00a0e2 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-05-11 12:41:50.256071", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_5", + "description" + ], + "fields": [ + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-06-21 12:54:37.764712", + "modified_by": "Administrator", + "module": "HR", + "name": "Grievance Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.py b/erpnext/hr/doctype/grievance_type/grievance_type.py new file mode 100644 index 0000000000..618cf0a031 --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class GrievanceType(Document): + pass diff --git a/erpnext/hr/doctype/grievance_type/test_grievance_type.py b/erpnext/hr/doctype/grievance_type/test_grievance_type.py new file mode 100644 index 0000000000..a02a34d41f --- /dev/null +++ b/erpnext/hr/doctype/grievance_type/test_grievance_type.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestGrievanceType(unittest.TestCase): + pass diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index c5201c22c9..4500ba4560 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -153,6 +153,24 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grievance Type", + "link_to": "Grievance Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Employee Grievance", + "link_to": "Employee Grievance", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Employee", "hidden": 0, @@ -823,7 +841,7 @@ "type": "Link" } ], - "modified": "2021-04-26 13:36:15.413819", + "modified": "2021-05-13 17:19:40.524444", "modified_by": "Administrator", "module": "HR", "name": "HR", diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js index d1ed91fac7..24ffce537c 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.js +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js @@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', { } }; }); + }, - frm.trigger('set_earning_component'); + onload: function(frm) { + if (frm.doc.type) { + frm.trigger('set_component_query'); + } }, employee: function(frm) { @@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', { }, company: function(frm) { - frm.trigger('set_earning_component'); + frm.set_value("type", ""); + frm.trigger('set_component_query'); }, - set_earning_component: function(frm) { + set_component_query: function(frm) { if (!frm.doc.company) return; + let filters = {company: frm.doc.company}; + if (frm.doc.type) { + filters.type = frm.doc.type; + } frm.set_query("salary_component", function() { return { - filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} + filters: filters }; }); }, diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 9e7db977ab..ce88cc3f1e 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", {"user_id": user}) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index dce6b7aa3d..e7d123c996 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, "doctype": "Salary Structure", "name": salary_structure, "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), + "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), "payroll_frequency": payroll_frequency, "payment_account": get_random("Account", filters={'account_currency': currency}), "currency": currency From 32d7b1f6ad21ab588a66e2fefd7ca5a413710db3 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 11:24:05 +0530 Subject: [PATCH 184/550] fix(Sales Invoice): Fix GL Entry creation for Return Invoices linked with Assets --- .../doctype/sales_invoice/sales_invoice.py | 10 ++-- erpnext/assets/doctype/asset/depreciation.py | 52 ++++++++++++++----- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 58a2a33473..347d2f58ab 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.assets.doctype.asset.depreciation \ - import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal + import get_disposal_account_and_cost_center, get_gl_entries_on_asset_movement from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no from erpnext.setup.doctype.company.company import update_company_current_month_sales @@ -928,8 +928,12 @@ class SalesInvoice(SellingController): frappe.throw(_("Select finance book for the item {0} at row {1}") .format(item.item_code, item.idx)) - fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, - item.base_net_amount, item.finance_book) + if self.is_return: + fixed_asset_gl_entries = get_gl_entries_on_asset_movement(asset, + item.base_net_amount, item.finance_book, True) + else: + fixed_asset_gl_entries = get_gl_entries_on_asset_movement(asset, + item.base_net_amount, item.finance_book) for gle in fixed_asset_gl_entries: gle["against"] = self.customer diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 8f0afb42b2..a18f4278e1 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -147,7 +147,7 @@ def scrap_asset(asset_name): je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) - for entry in get_gl_entries_on_asset_disposal(asset): + for entry in get_gl_entries_on_asset_movement(asset): entry.update({ "reference_type": "Asset", "reference_name": asset_name @@ -177,7 +177,7 @@ def restore_asset(asset_name): asset.set_status() @frappe.whitelist() -def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): +def get_gl_entries_on_asset_movement(asset, selling_amount=0, finance_book=None, is_return = False): fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -193,6 +193,44 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) if asset.calculate_depreciation else asset.value_after_depreciation) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) + if is_return: + gl_entries = get_gl_entries_on_asset_regain(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount) + profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) + + else: + gl_entries = get_gl_entries_on_asset_disposal(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount) + profit_amount = flt(selling_amount) - flt(value_after_depreciation) + + if profit_amount: + debit_or_credit = "debit" if profit_amount < 0 else "credit" + gl_entries.append({ + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount) + }) + + return gl_entries + +def get_gl_entries_on_asset_regain(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount): + gl_entries = [ + { + "account": fixed_asset_account, + "debit_in_account_currency": asset.gross_purchase_amount, + "debit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center + }, + { + "account": accumulated_depr_account, + "credit_in_account_currency": accumulated_depr_amount, + "credit": accumulated_depr_amount, + "cost_center": depreciation_cost_center + } + ] + + return gl_entries + +def get_gl_entries_on_asset_disposal(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount): gl_entries = [ { "account": fixed_asset_account, @@ -208,16 +246,6 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) } ] - profit_amount = flt(selling_amount) - flt(value_after_depreciation) - if profit_amount: - debit_or_credit = "debit" if profit_amount < 0 else "credit" - gl_entries.append({ - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount) - }) - return gl_entries @frappe.whitelist() From 88348e2da713603670506aef566c300566daf054 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 11:57:53 +0530 Subject: [PATCH 185/550] fix(Sales Invoice): Print appropriate message if Asset isn't specified when the Item is a Fixed Asset --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 347d2f58ab..494f63f4a2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -920,9 +920,10 @@ class SalesInvoice(SellingController): if item.get('asset'): asset = frappe.get_doc("Asset", item.asset) else: - frappe.throw(_("Enter Asset linked with Item {0}: {1} in row {2}.") - .format(item.item_code, item.item_name, item.idx)) - + frappe.throw(_( + "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), + title=_("Missing Asset") + ) if (len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book): frappe.throw(_("Select finance book for the item {0} at row {1}") From 44815393b375b0a97d461b93595133262fbcb499 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:28:02 +0530 Subject: [PATCH 186/550] fix: job applicant link issue (#25934) --- erpnext/hr/doctype/job_applicant/job_applicant_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js index 3b9141ba79..2ad0d591d8 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_list.js +++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js @@ -2,7 +2,7 @@ // MIT License. See license.txt frappe.listview_settings['Job Applicant'] = { - add_fields: ["company", "designation", "job_applicant", "status"], + add_fields: ["status"], get_indicator: function (doc) { if (doc.status == "Accepted") { return [__(doc.status), "green", "status,=," + doc.status]; From 40a4330ec1240492484d67bbdd0eeb777440c34f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 12:38:37 +0530 Subject: [PATCH 187/550] fix: Move tax categories up in country wise json --- .../setup_wizard/data/country_wise_tax.json | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 6df57f1738..e36bf5cbe0 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1164,35 +1164,35 @@ }, "India": { + "tax_categories": [ + { + "title": "In-State", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Out-State", + "is_inter_state": 1, + "gst_state": "" + }, + { + "title": "Reverse Charge In-State", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Reverse Charge Out-State", + "is_inter_state": 1, + "gst_state": "" + }, + { + "title": "Registered Composition", + "is_inter_state": 0, + "gst_state": "" + } + ], "chart_of_accounts": { "*": { - "tax_categories": [ - { - "title": "In-State", - "is_inter_state": 0, - "gst_state": "" - }, - { - "title": "Out-State", - "is_inter_state": 1, - "gst_state": "" - }, - { - "title": "Reverse Charge In-State", - "is_inter_state": 0, - "gst_state": "" - }, - { - "title": "Reverse Charge Out-State", - "is_inter_state": 0, - "gst_state": "" - }, - { - "title": "Registered Composition", - "is_inter_state": 0, - "gst_state": "" - } - ], "item_tax_templates": [ { "title": "GST 9%", From 208b5f9e7303bd89b694aa569c6143d4656c4c6b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 12:44:56 +0530 Subject: [PATCH 188/550] chore: Add comments --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index a664914f08..d5682b6f4c 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -143,6 +143,9 @@ def make_taxes_and_charges_template(company_name, doctype, template): tax_row[fieldname] = default_value doc = frappe.get_doc(template) + + # Data in country wise json is already pre validated, hence validations can be ignored + # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) @@ -172,6 +175,9 @@ def make_item_tax_template(company_name, template): tax_row['tax_rate'] = account_data.get('tax_rate') doc = frappe.get_doc(template) + + # Data in country wise json is already pre validated, hence validations can be ignored + # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) From 35e11fbea6baeb140a08bee8a30a2d156f20e7b1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 13:17:01 +0530 Subject: [PATCH 189/550] fix: Tests --- erpnext/setup/setup_wizard/operations/install_fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 5c725d332d..9c7d9e5259 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -480,7 +480,7 @@ def update_stock_settings(): stock_settings.save() def create_bank_account(args): - if not args.bank_account: + if not args.get('bank_account'): return company_name = args.company_name From 80399802c62794f5002937eb291525e3934de5fc Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 13:26:47 +0530 Subject: [PATCH 190/550] fix(Asset Repair): Replace asset_value with value_after_depreciation in tests --- .../doctype/asset_repair/test_asset_repair.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index d1b417fd38..52a960e850 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -74,21 +74,21 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) def test_increase_in_asset_value_due_to_stock_consumption(self): - asset = create_asset() - initial_asset_value = asset.asset_value + asset = create_asset(calculate_depreciation = 1) + initial_asset_value = get_asset_value(asset) asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1) asset.reload() - increase_in_asset_value = asset.asset_value - initial_asset_value + increase_in_asset_value = get_asset_value(asset) - initial_asset_value self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): - asset = create_asset() - initial_asset_value = asset.asset_value + asset = create_asset(calculate_depreciation = 1) + initial_asset_value = get_asset_value(asset) asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1) asset.reload() - increase_in_asset_value = asset.asset_value - initial_asset_value + increase_in_asset_value = get_asset_value(asset) - initial_asset_value self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) def test_purchase_invoice(self): @@ -109,6 +109,9 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation) +def get_asset_value(asset): + return asset.finance_books[0].value_after_depreciation + def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations From 9ec0ded28ff5e2aea03d6b015e272c9d69209792 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 23 Jun 2021 14:05:10 +0530 Subject: [PATCH 191/550] fix: Staffing plan vacancies data type issue (#25941) * fix: staffing plan vacancies data type issue * fix: translation issue * fix: removed greater than 0 condition --- .../hr/doctype/staffing_plan/staffing_plan.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 533149a823..e6c783aca2 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -41,7 +41,7 @@ class StaffingPlan(Document): detail.total_estimated_cost = 0 if detail.number_of_positions > 0: - if detail.vacancies > 0 and detail.estimated_cost_per_position: + if detail.vacancies and detail.estimated_cost_per_position: detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) self.total_estimated_budget += detail.total_estimated_cost @@ -76,12 +76,12 @@ class StaffingPlan(Document): if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ - for {2} as per staffing plan {3} for parent company {4}." - .format(cint(parent_plan_details[0].vacancies), + for {2} as per staffing plan {3} for parent company {4}.").format( + cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_plan_details[0].name, - parent_company)), ParentCompanyError) + parent_company), ParentCompanyError) #Get vacanices already planned for all companies down the hierarchy of Parent Company lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) @@ -98,14 +98,14 @@ class StaffingPlan(Document): (flt(parent_plan_details[0].total_estimated_cost) < \ (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ - You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." - .format(cint(all_sibling_details.vacancies), + You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format( + cint(all_sibling_details.vacancies), all_sibling_details.total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_company, cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, - parent_plan_details[0].name))) + parent_plan_details[0].name)) def validate_with_subsidiary_plans(self, staffing_plan_detail): #Valdate this plan with all child company plan @@ -121,11 +121,11 @@ class StaffingPlan(Document): cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ - Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" - .format(self.company, + Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format( + self.company, cint(children_details.vacancies), children_details.total_estimated_cost, - frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) + frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError) @frappe.whitelist() def get_designation_counts(designation, company): @@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now designation, from_date, to_date) # Only a single staffing plan can be active for a designation on given date - return staffing_plan if staffing_plan else None \ No newline at end of file + return staffing_plan if staffing_plan else None From d802d7397313a857fca97c8f7f7a190a11fa5e5f Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 14:08:07 +0530 Subject: [PATCH 192/550] fix: Consider Website Item Groups in Item group page product listing - Passed an argument to query engine to know when query is for item group page - If for item group page, get data with regards to website item group table - This query should be fast since there's one filter and that shortens the table beforehand - This data is merged with the results from the Item master (results only considering item attributes and field filters) - The combined data is then sorted as per weightage Co-authored-by: Gavin D'souza --- .../setup/doctype/item_group/item_group.py | 2 +- erpnext/shopping_cart/product_query.py | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index db31d6d699..1c72cebfa9 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): field_filters['item_group'] = self.name engine = ProductQuery() - context.items = engine.query(attribute_filters, field_filters, search, start) + context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) filter_engine = ProductFiltersBuilder(self.name) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index dd94c26bc6..bb31220447 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -22,13 +22,14 @@ class ProductQuery: self.settings = frappe.get_doc("Products Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route'] + self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', + 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage'] self.filters = [] self.or_filters = [['show_in_website', '=', 1]] if not self.settings.get('hide_variants'): self.or_filters.append(['show_variant_in_website', '=', 1]) - def query(self, attributes=None, fields=None, search_term=None, start=0): + def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """Summary Args: @@ -44,6 +45,15 @@ class ProductQuery: if search_term: self.build_search_filters(search_term) result = [] + website_item_groups = [] + + # if from item group page consider website item group table + if item_group: + website_item_groups = frappe.db.get_all( + "Item", + fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], + filters=[["Website Item Group", "item_group", "=", item_group]] + ) if attributes: all_items = [] @@ -61,12 +71,10 @@ class ProductQuery: ], or_filters=self.or_filters, start=start, - limit=self.page_length, - order_by="weightage desc" + limit=self.page_length ) items_dict = {item.name: item for item in items} - # TODO: Replace Variants by their parent templates all_items.append(set(items_dict.keys())) @@ -78,14 +86,22 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, start=start, - limit=self.page_length, - order_by="weightage desc" + limit=self.page_length ) + # Combine results having context of website item groups into item results + if item_group and website_item_groups: + items_list = {row.name for row in result} + for row in website_item_groups: + if row.wig_parent not in items_list: + result.append(row) + + result = sorted(result, key=lambda x: x.get("weightage"), reverse=True) + for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None + item.formatted_price = product_info.get('price', {}).get('formatted_price') return result From 0bfffddac470cb003efba73d4ee6c1bca2620609 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 15:37:17 +0530 Subject: [PATCH 193/550] fix: Test Cases --- erpnext/setup/setup_wizard/operations/install_fixtures.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 9c7d9e5259..7dfb9f4d3c 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -483,14 +483,14 @@ def create_bank_account(args): if not args.get('bank_account'): return - company_name = args.company_name + company_name = args.get('company_name') bank_account_group = frappe.db.get_value("Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name}) if bank_account_group: bank_account = frappe.get_doc({ "doctype": "Account", - 'account_name': args.bank_account, + 'account_name': args.get('bank_account'), 'parent_account': bank_account_group, 'is_group':0, 'company': company_name, @@ -499,10 +499,10 @@ def create_bank_account(args): try: doc = bank_account.insert() - frappe.db.set_value("Company", args.company_name, "default_bank_account", bank_account.name, update_modified=False) + frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.bank_account)) + frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account'))) except frappe.DuplicateEntryError: # bank account same as a CoA entry pass From 1b9b72d12eac988f03b1feda17f6524c96ab5b72 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 16:03:24 +0530 Subject: [PATCH 194/550] fix: Filters did not consider Website Item Group --- erpnext/shopping_cart/filters.py | 21 +++++++++++++++------ erpnext/shopping_cart/product_query.py | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 6c63d8759b..979afd3c13 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -22,12 +22,15 @@ class ProductFiltersBuilder: filter_data = [] for df in fields: - filters = {} + filters, or_filters = {}, [] if df.fieldtype == "Link": if self.item_group: - filters['item_group'] = self.item_group + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() @@ -44,7 +47,9 @@ class ProductFiltersBuilder: values = [d.name for d in frappe.get_all(doctype, filters)] # Remove None - values = values.remove(None) if None in values else values + if None in values: + values.remove(None) + if values: filter_data.append([df, values]) @@ -61,14 +66,18 @@ class ProductFiltersBuilder: for attr_doc in attribute_docs: selected_attributes = [] for attr in attr_doc.item_attribute_values: + or_filters = [] filters= [ ["Item Variant Attribute", "attribute", "=", attr.parent], ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] ] if self.item_group: - filters.append(["item_group", "=", self.item_group]) + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - if frappe.db.get_all("Item", filters, limit=1): + if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1): selected_attributes.append(attr) if selected_attributes: diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index bb31220447..0b05f68ae9 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -101,7 +101,7 @@ class ProductQuery: for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info.get('price', {}).get('formatted_price') + item.formatted_price = (product_info.get('price') or {}).get('formatted_price') return result From f913e0dd05b41921d8ad8c6f0fa1620bb1adc545 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:06:11 +0530 Subject: [PATCH 195/550] fix: Consider Table Multiselect fields in Query engine - Since table multiselect fields were not handled, the query tried searching for this child field in item master - This broke the query - On trying to reload or go back to all-products page with field filters that are table mutiselect, page breaks --- erpnext/shopping_cart/product_query.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 0b05f68ae9..cd4a176921 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -115,6 +115,17 @@ class ProductQuery: if not values: continue + # handle multiselect fields in filter addition + meta = frappe.get_meta('Item', cached=True) + df = meta.get_field(field) + if df.fieldtype == 'Table MultiSelect': + child_doctype = df.options + child_meta = frappe.get_meta(child_doctype, cached=True) + fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + if fields: + self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) + continue + if isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) From 078826d510aef7d1a1a0e3bce63c451f1d47e727 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:12:59 +0530 Subject: [PATCH 196/550] fix: Sider --- erpnext/shopping_cart/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 979afd3c13..9f06d20bde 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() From 1f7b95f39039981fb20e5040d1b9fe68fa78fb59 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 20:56:27 +0530 Subject: [PATCH 197/550] fix: User is not able to change item tax template --- .../public/js/controllers/taxes_and_totals.js | 9 +++++---- erpnext/stock/get_item_details.py | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index e5a5fcfe3b..4a14a665cd 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -270,11 +270,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ let me = this; let item_codes = []; let item_rates = {}; + let item_tax_templates = {}; + $.each(this.frm.doc.items || [], function(i, item) { if (item.item_code) { // Use combination of name and item code in case same item is added multiple times item_codes.push([item.item_code, item.name]); item_rates[item.name] = item.net_rate; + item_tax_templates[item.name] = item.item_tax_template } }); @@ -285,18 +288,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ company: me.frm.doc.company, tax_category: cstr(me.frm.doc.tax_category), item_codes: item_codes, + item_tax_templates: item_tax_templates, item_rates: item_rates }, callback: function(r) { if (!r.exc) { $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name)) { + if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { item.item_tax_template = r.message[item.name].item_tax_template; item.item_tax_rate = r.message[item.name].item_tax_rate; me.add_taxes_from_item_tax_template(item.item_tax_rate); - } else { - item.item_tax_template = ""; - item.item_tax_rate = "{}"; } }); } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 746cbbf601..bab004ec92 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,7 +436,7 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_tax_templates, item_rates=None): out = {} if isinstance(item_codes, string_types): item_codes = json.loads(item_codes) @@ -444,12 +444,18 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None): if isinstance(item_rates, string_types): item_rates = json.loads(item_rates) + if isinstance(item_tax_templates, string_types): + item_tax_templates = json.loads(item_tax_templates) + for item_code in item_codes: - if not item_code or item_code[1] in out: + if not item_code or item_code[1] in out or not item_tax_templates.get(item_code[1]): continue + out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]], + "item_tax_template": item_tax_templates.get(item_code[1])} + get_item_tax_template(args, item, out[item_code[1]]) out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) @@ -463,9 +469,7 @@ def get_item_tax_template(args, item, out): } """ item_tax_template = args.get("item_tax_template") - - if not item_tax_template: - item_tax_template = _get_item_tax_template(args, item.taxes, out) + item_tax_template = _get_item_tax_template(args, item.taxes, out) if not item_tax_template: item_group = item.item_group @@ -508,7 +512,8 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return None # do not change if already a valid template - if args.get('item_tax_template') in taxes: + if args.get('item_tax_template') in [t.item_tax_template for t in taxes]: + out["item_tax_template"] = args.get('item_tax_template') return args.get('item_tax_template') for tax in taxes: From 2f5f9d8566a57fd3b9781cfbabfe71583dafea62 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 22:19:05 +0530 Subject: [PATCH 198/550] fix(Asset): Fix value_after_depreciation calculation --- erpnext/assets/doctype/asset/asset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 27d21e2542..29379657a1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -182,10 +182,10 @@ class Asset(AccountsController): # value_after_depreciation - current Asset value if d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + flt(self.opening_accumulated_depreciation)) else: value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) - flt(d.expected_value_after_useful_life) + flt(self.opening_accumulated_depreciation)) d.value_after_depreciation = value_after_depreciation From dce21137364b86e997a2d000c00c25b4d2c78684 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 23 Jun 2021 22:26:45 +0530 Subject: [PATCH 199/550] fix(Asset Repair): Remove test that's no longer necessary --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 52a960e850..b3d78b3bfb 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -13,12 +13,6 @@ class TestAssetRepair(unittest.TestCase): create_asset_data() frappe.db.sql("delete from `tabTax Rule`") - def test_completion_date(self): - asset_repair = create_asset_repair() - asset_repair.repair_status = "Completed" - asset_repair.save() - self.assertTrue(asset_repair.completion_date) - def test_update_status(self): asset = create_asset() initial_status = asset.status From c9c1d10435a327db4b19c4529802a01aa19ccf31 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 22:47:29 +0530 Subject: [PATCH 200/550] fix: Make item tax templates optional --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bab004ec92..773a18fbf9 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,7 +436,7 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_tax_templates, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None, item_rates=None): out = {} if isinstance(item_codes, string_types): item_codes = json.loads(item_codes) From 7e006496dd199c5c46e2e9cc77f2868583c53d16 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 22:52:51 +0530 Subject: [PATCH 201/550] fix: Check if item tax template exists --- erpnext/stock/get_item_details.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 773a18fbf9..37850350ab 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -453,8 +453,10 @@ def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]], - "item_tax_template": item_tax_templates.get(item_code[1])} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + + if item_tax_templates: + args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) get_item_tax_template(args, item, out[item_code[1]]) out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) From f5fa1ee4b65c5f38c1398da934fdc2cd25a09777 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 11:03:32 +0530 Subject: [PATCH 202/550] fix: Country Link field in 'Add address' website modal auto-clears --- erpnext/templates/includes/cart/cart_address.html | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 84a9430956..4482bc10cf 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -99,6 +99,7 @@ frappe.ready(() => { fieldname: 'country', fieldtype: 'Link', options: 'Country', + only_select: true, reqd: 1 }, { From a9b5dc6030c3a25f65793170f1b8a05b78a3ba9a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 24 Jun 2021 11:53:28 +0530 Subject: [PATCH 203/550] fix: chart not visible for First Response Time reports (#26032) (#26185) --- .../first_response_time_for_opportunity.js | 7 +++---- .../first_response_time_for_issues.js | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js index 3f5c95ab0a..fe5707af29 100644 --- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js @@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = { get_chart_data: function (_columns, result) { return { data: { - labels: result.map(d => d[0]), + labels: result.map(d => d.creation_date), datasets: [{ name: "First Response Time", - values: result.map(d => d[1]) + values: result.map(d => d.first_response_time) }] }, type: "line", @@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = { hide_days: 0, hide_seconds: 0 }; - value = frappe.utils.get_formatted_duration(d, duration_options); - return value; + return frappe.utils.get_formatted_duration(d, duration_options); } } } diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js index 576e0b76da..18691fe264 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js @@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Issues"] = { get_chart_data: function(_columns, result) { return { data: { - labels: result.map(d => d[0]), + labels: result.map(d => d.creation_date), datasets: [{ name: 'First Response Time', - values: result.map(d => d[1]) + values: result.map(d => d.first_response_time) }] }, type: "line", @@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Issues"] = { hide_days: 0, hide_seconds: 0 }; - value = frappe.utils.get_formatted_duration(d, duration_options); - return value; + return frappe.utils.get_formatted_duration(d, duration_options); } } } From bbe64b560446e5e812d55a0bb104e0fbb4f2a683 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 12:01:12 +0530 Subject: [PATCH 204/550] fix: (style) Address card buttons hover state --- erpnext/public/scss/shopping_cart.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 9402cf9ea4..5962859be5 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -467,11 +467,15 @@ body.product-page { .btn-change-address { color: var(--blue-500); - box-shadow: none; - border: 1px solid var(--blue-500); } } +.btn-new-address:hover, .btn-change-address:hover { + box-shadow: none; + color: var(--blue-500) !important; + border: 1px solid var(--blue-500); +} + .modal .address-card { .card-body { padding: var(--padding-sm); From bd9317956beb1ffe4aaefd59cee72f39d9a7ad4f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 19:48:08 +0530 Subject: [PATCH 205/550] fix: Taxes on Internal Transfer payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b6b2bef963..adaf99a790 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -706,7 +706,7 @@ class PaymentEntry(AccountsController): if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if self.payment_type == 'Pay': + if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" @@ -761,7 +761,7 @@ class PaymentEntry(AccountsController): return self.advance_tax_account elif self.payment_type == 'Receive': return self.paid_from - elif self.payment_type == 'Pay': + elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to def update_advance_paid(self): From 9d8e8f8bdfabbbc8b3721d354bf441098b212933 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Jun 2021 20:38:35 +0530 Subject: [PATCH 206/550] fix: Do not show received amount after tax for internal tarnsfers --- erpnext/accounts/doctype/payment_entry/payment_entry.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 54623dd6cd..51f18a5a4e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -690,7 +690,7 @@ "options": "Account" }, { - "depends_on": "eval:doc.received_amount", + "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", "label": "Received Amount After Tax", @@ -707,7 +707,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-09 11:55:04.215050", + "modified": "2021-06-22 20:37:06.154206", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 60a44ae1e63dafa8d9b15b1d11a479f74c196a73 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 12:44:13 +0530 Subject: [PATCH 207/550] fix(Asset): Fix test --- erpnext/assets/doctype/asset/test_asset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index f3667c7b95..32bdb5224a 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": "2030-12-31" }) - asset.insert() self.assertEqual(asset.status, "Draft") asset.save() expected_schedules = [ From a4f5dcaa9ab610c10de24a1bc0c835cd615d6a09 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 22:38:10 +0530 Subject: [PATCH 208/550] chore: Test for Item visibility in multiple item group pages --- .../test_product_configurator.py | 63 +++++++++++++++++++ erpnext/shopping_cart/filters.py | 2 +- erpnext/shopping_cart/product_query.py | 6 +- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index 3521e7e8bf..daaba67173 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -43,6 +43,30 @@ class TestProductConfigurator(unittest.TestCase): "show_variant_in_website": 1 }).insert() + def create_regular_web_item(self, name, item_group=None): + if not frappe.db.exists('Item', name): + doc = frappe.get_doc({ + "description": name, + "item_code": name, + "item_name": name, + "doctype": "Item", + "is_stock_item": 1, + "item_group": item_group or "_Test Item Group", + "stock_uom": "_Test UOM", + "item_defaults": [{ + "company": "_Test Company", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + "income_account": "Sales - _TC" + }], + "show_in_website": 1 + }).insert() + else: + doc = frappe.get_doc("Item", name) + return doc + def test_product_list(self): template_items = frappe.get_all('Item', {'show_in_website': 1}) variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) @@ -79,3 +103,42 @@ class TestProductConfigurator(unittest.TestCase): 'Test Size': ['2XL'] }) self.assertEqual(len(items), 1) + + def test_products_in_multiple_item_groups(self): + """Check if product is visible on multiple item group pages barring its own.""" + from erpnext.shopping_cart.product_query import ProductQuery + + if not frappe.db.exists("Item Group", {"name": "Tech Items"}): + item_group_doc = frappe.get_doc({ + "doctype": "Item Group", + "item_group_name": "Tech Items", + "parent_item_group": "All Item Groups", + "show_in_website": 1 + }).insert() + else: + item_group_doc = frappe.get_doc("Item Group", "Tech Items") + + doc = self.create_regular_web_item("Portal Item", item_group="Tech Items") + if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}): + doc.append("website_item_groups", { + "item_group": "_Test Item Group Desktops" + }) + doc.save() + + # check if item is visible in its own Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") + self.assertEqual(len(items), 1) + self.assertEqual(items[0].item_code, "Portal Item") + + # check if item is visible in configured foreign Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") + item_codes = [row.item_code for row in items] + + self.assertIn(len(items), [2, 3]) + self.assertIn("Portal Item", item_codes) + + # teardown + doc.delete() + item_group_doc.delete() \ No newline at end of file diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 9f06d20bde..7dfa09e2d6 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname) else: doctype = df.get_link_doctype() diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index cd4a176921..d96d803416 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -121,12 +121,10 @@ class ProductQuery: if df.fieldtype == 'Table MultiSelect': child_doctype = df.options child_meta = frappe.get_meta(child_doctype, cached=True) - fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + fields = child_meta.get("fields") if fields: self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) - continue - - if isinstance(values, list): + elif isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) else: From 550fe8ae395e6bf4b5598ed01fa81229fc8955ce Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 13:22:26 +0530 Subject: [PATCH 209/550] fix(Asset): Remove redundant code --- erpnext/assets/doctype/asset/test_asset.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 32bdb5224a..59fbe3b030 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -153,9 +153,8 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": '2030-12-31' }) - asset.insert() - self.assertEqual(asset.status, "Draft") asset.save() + self.assertEqual(asset.status, "Draft") expected_schedules = [ ['2030-12-31', 66667.00, 66667.00], @@ -184,7 +183,7 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 12, "depreciation_start_date": "2030-12-31" }) - asset.insert() + asset.save() self.assertEqual(asset.status, "Draft") expected_schedules = [ @@ -215,7 +214,6 @@ class TestAsset(unittest.TestCase): "depreciation_start_date": "2030-12-31" }) - asset.insert() asset.save() expected_schedules = [ @@ -246,7 +244,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() asset.load_from_db() self.assertEqual(asset.status, "Submitted") @@ -349,7 +346,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() post_depreciation_entries(date="2021-01-01") @@ -379,7 +375,6 @@ class TestAsset(unittest.TestCase): "total_number_of_depreciations": 10, "frequency_of_depreciation": 1 }) - asset.insert() asset.submit() post_depreciation_entries(date=add_months('2020-01-01', 4)) @@ -423,7 +418,6 @@ class TestAsset(unittest.TestCase): "frequency_of_depreciation": 10, "depreciation_start_date": "2020-12-31" }) - asset.insert() asset.submit() post_depreciation_entries(date="2021-01-01") @@ -467,7 +461,7 @@ class TestAsset(unittest.TestCase): "total_number_of_depreciations": 3, "frequency_of_depreciation": 10 }) - asset.insert() + asset.save() accumulated_depreciation_after_full_schedule = \ max(d.accumulated_depreciation_amount for d in asset.get("schedules")) From ca2e9147151546390e1ff5dcd964b7842dc808c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 14:14:46 +0530 Subject: [PATCH 210/550] fix: Error while booking deferred revenue --- erpnext/accounts/deferred_revenue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index dd346bc240..2f86c6c1de 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): amount, base_amount = calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency) + if not amount: + return + if via_journal_entry: book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) From 98e98d25e652bc114e2f59e58c3621887f6f7700 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 24 Jun 2021 14:24:28 +0530 Subject: [PATCH 211/550] fix(Work Order): added freeze when trying to stop work order (#26192) (#26196) * fix: added freeze when trying to stop work order * fix(ux): add freeze message Co-authored-by: Noah Jacob --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3e5a72db9a..8088d930df 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -704,6 +704,8 @@ erpnext.work_order = { stop_work_order: function(frm, status) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop", + freeze: true, + freeze_message: __("Updating Work Order status"), args: { work_order: frm.doc.name, status: status From 7fd44907ba382ef2cb6778183a0bd7801af1b7a2 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 24 Jun 2021 14:26:36 +0530 Subject: [PATCH 212/550] feat: fetching of qty as per received qty from PR to PI (#26184) --- .../doctype/buying_settings/buying_settings.json | 14 +++++++++++--- erpnext/controllers/accounts_controller.py | 10 ++++++++-- erpnext/patches.txt | 1 + ...ll_for_rejected_quantity_in_purchase_invoice.py | 8 ++++++++ .../doctype/purchase_receipt/purchase_receipt.py | 8 ++++++-- .../purchase_receipt/test_purchase_receipt.py | 7 +++++++ 6 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 630a1dc8cd..b9c77d59b1 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -9,13 +9,14 @@ "supp_master_name", "supplier_group", "buying_price_list", + "maintain_same_rate_action", + "role_to_override_stop_action", "column_break_3", "po_required", "pr_required", "maintain_same_rate", - "maintain_same_rate_action", - "role_to_override_stop_action", "allow_multiple_items", + "bill_for_rejected_quantity_in_purchase_invoice", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -108,6 +109,13 @@ "fieldtype": "Link", "label": "Role Allowed to Override Stop Action", "options": "Role" + }, + { + "default": "1", + "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", + "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", + "fieldtype": "Check", + "label": "Bill for Rejected Quantity in Purchase Invoice" } ], "icon": "fa fa-cog", @@ -115,7 +123,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-04 20:01:44.087066", + "modified": "2021-06-24 10:38:28.934525", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 243939b275..1c086e9edc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -828,8 +828,14 @@ class AccountsController(TransactionBase): role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles(): - frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") - .format(item.item_code, item.idx, max_allowed_amt)) + if self.doctype != "Purchase Invoice": + self.throw_overbill_exception(item, max_allowed_amt) + elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")): + self.throw_overbill_exception(item, max_allowed_amt) + + def throw_overbill_exception(self, item, max_allowed_amt): + frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") + .format(item.item_code, item.idx, max_allowed_amt)) def get_company_default(self, fieldname): from erpnext.accounts.utils import get_company_default diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ed6fefdd87..dd0e33beba 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -288,3 +288,4 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold +erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice \ No newline at end of file diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py new file mode 100644 index 0000000000..be85cfdeef --- /dev/null +++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doctype("Buying Settings") + buying_settings = frappe.get_single("Buying Settings") + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0 + buying_settings.save() \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b8580f95a3..e488b695b5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -581,7 +581,6 @@ def update_billing_percentage(pr_doc, update_modified=True): @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): - from frappe.model.mapper import get_mapped_doc from erpnext.accounts.party import get_payment_terms_template doc = frappe.get_doc('Purchase Receipt', source_name) @@ -601,11 +600,16 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + target_doc.rejected_qty = 0 target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): - pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) + qty = item_row.qty + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + qty = item_row.received_qty + pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = flt(returned_qty_map.get(item_row.name, 0)) if returned_qty: if returned_qty >= pending_qty: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 95096d77d7..07c5da1dca 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -421,11 +421,18 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(return_pr_2.items[0].qty, -3) # Make PI against unreturned amount + buying_settings = frappe.get_single("Buying Settings") + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0 + buying_settings.save() + pi = make_purchase_invoice(pr.name) pi.submit() self.assertEqual(pi.items[0].qty, 3) + buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 1 + buying_settings.save() + pr.load_from_db() # PR should be completed on billing all unreturned amount self.assertEqual(pr.items[0].billed_amt, 150) From 2fdb923953b4ed84b55771fbc07dc1556d7ac1f1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 14:56:34 +0530 Subject: [PATCH 213/550] fix(Asset Repair): Change controller hooks --- erpnext/assets/doctype/asset_repair/asset_repair.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 6054258ea6..64c51fd8c3 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -35,7 +35,7 @@ class AssetRepair(AccountsController): total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() self.total_repair_cost += total_value_of_stock_consumed - def on_submit(self): + def before_submit(self): self.check_repair_status() if self.get('stock_consumption') or self.get('capitalize_repair_cost'): @@ -52,7 +52,7 @@ class AssetRepair(AccountsController): self.asset_doc.prepare_depreciation_data() self.asset_doc.save() - def on_cancel(self): + def before_cancel(self): self.asset_doc = frappe.get_doc('Asset', self.asset) if self.get('stock_consumption') or self.get('capitalize_repair_cost'): From 54cc1dedf2138a41fbd2d3a9247a4970fb69572c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Jun 2021 21:18:20 +0530 Subject: [PATCH 214/550] refactor(pos): use pos invoice item name as unique identifier --- .../page/point_of_sale/pos_controller.js | 123 ++++++++++-------- .../page/point_of_sale/pos_item_cart.js | 29 +---- .../page/point_of_sale/pos_item_details.js | 86 +++++------- .../page/point_of_sale/pos_item_selector.js | 6 +- 4 files changed, 113 insertions(+), 131 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ae3f9e3c9d..4c938756c7 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -241,8 +241,8 @@ erpnext.PointOfSale.Controller = class { events: { get_frm: () => this.frm, - cart_item_clicked: (item_code, batch_no, uom, rate) => { - const item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); + cart_item_clicked: (item) => { + const item_row = this.get_item_from_frm(item); this.item_details.toggle_item_details_section(item_row); }, @@ -273,17 +273,16 @@ erpnext.PointOfSale.Controller = class { this.cart.toggle_numpad(minimize); }, - form_updated: (cdt, cdn, fieldname, value) => { - const item_row = frappe.model.get_doc(cdt, cdn); - if (item_row && item_row[fieldname] != value) { + form_updated: (item, field, value) => { + const item_row = frappe.model.get_doc(item.doctype, item.name); + if (item_row && item_row[field] != value) { - const { item_code, batch_no, uom, rate } = this.item_details.current_item; - const event = { - field: fieldname, + const args = { + field, value, - item: { item_code, batch_no, uom, rate } + item: this.item_details.current_item } - return this.on_cart_update(event) + return this.on_cart_update(args) } return Promise.resolve(); @@ -300,19 +299,18 @@ erpnext.PointOfSale.Controller = class { set_value_in_current_cart_item: (selector, value) => { this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item); }, - clone_new_batch_item_in_frm: (batch_serial_map, current_item) => { + clone_new_batch_item_in_frm: (batch_serial_map, item) => { // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches // for each unique batch new item row is added in the form & cart Object.keys(batch_serial_map).forEach(batch => { - const { item_code, batch_no } = current_item; - const item_to_clone = this.frm.doc.items.find(i => i.item_code === item_code && i.batch_no === batch_no); + const item_to_clone = this.frm.doc.items.find(i => i.name == item.name); const new_row = this.frm.add_child("items", { ...item_to_clone }); // update new serialno and batch new_row.batch_no = batch; new_row.serial_no = batch_serial_map[batch].join(`\n`); new_row.qty = batch_serial_map[batch].length; this.frm.doc.items.forEach(row => { - if (item_code === row.item_code) { + if (item.item_code === row.item_code) { this.update_cart_html(row); } }); @@ -321,8 +319,8 @@ erpnext.PointOfSale.Controller = class { remove_item_from_cart: () => this.remove_item_from_cart(), get_item_stock_map: () => this.item_stock_map, close_item_details: () => { - this.item_details.toggle_item_details_section(undefined); - this.cart.prev_action = undefined; + this.item_details.toggle_item_details_section(null); + this.cart.prev_action = null; this.cart.toggle_item_highlight(); }, get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse) @@ -506,50 +504,47 @@ erpnext.PointOfSale.Controller = class { let item_row = undefined; try { let { field, value, item } = args; - const { item_code, batch_no, serial_no, uom, rate } = item; - item_row = this.get_item_from_frm(item_code, batch_no, uom, rate); + item_row = this.get_item_from_frm(item); + const item_row_exists = !$.isEmptyObject(item_row); - const item_selected_from_selector = field === 'qty' && value === "+1" + const from_selector = field === 'qty' && value === "+1"; + if (from_selector) + value = flt(item_row.qty) + flt(value); - if (item_row) { - item_selected_from_selector && (value = item_row.qty + flt(value)) - - field === 'qty' && (value = flt(value)); + if (item_row_exists) { + if (field === 'qty') + value = flt(value); if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); } - if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) { + if (this.is_current_item_being_edited(item_row) || from_selector) { await frappe.model.set_value(item_row.doctype, item_row.name, field, value); this.update_cart_html(item_row); } } else { - if (!this.frm.doc.customer) { - frappe.dom.unfreeze(); - frappe.show_alert({ - message: __('You must select a customer before adding an item.'), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); + if (!this.frm.doc.customer) + return this.raise_customer_selection_alert(); + + const { item_code, batch_no, serial_no, rate } = item; + + if (!item_code) return; - } - if (!item_code) return; - item_selected_from_selector && (value = flt(value)) - - const args = { item_code, batch_no, rate, [field]: value }; + const new_item = { item_code, batch_no, rate, [field]: value }; if (serial_no) { await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); - args['serial_no'] = serial_no; + new_item['serial_no'] = serial_no; } - if (field === 'serial_no') args['qty'] = value.split(`\n`).length || 0; + if (field === 'serial_no') + new_item['qty'] = value.split(`\n`).length || 0; - item_row = this.frm.add_child('items', args); + item_row = this.frm.add_child('items', new_item); if (field === 'qty' && value !== 0 && !this.allow_negative_stock) await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); @@ -558,8 +553,11 @@ erpnext.PointOfSale.Controller = class { this.update_cart_html(item_row); - this.item_details.$component.is(':visible') && this.edit_item_details_of(item_row); - this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row); + if (this.item_details.$component.is(':visible')) + this.edit_item_details_of(item_row); + + if (this.check_serial_batch_selection_needed(item_row)) + this.edit_item_details_of(item_row); } } catch (error) { @@ -570,14 +568,33 @@ erpnext.PointOfSale.Controller = class { } } - get_item_from_frm(item_code, batch_no, uom, rate) { - const has_batch_no = batch_no; - return this.frm.doc.items.find( - i => i.item_code === item_code - && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) - && (i.uom === uom) - && (i.rate == rate) - ); + raise_customer_selection_alert() { + frappe.dom.unfreeze(); + frappe.show_alert({ + message: __('You must select a customer before adding an item.'), + indicator: 'orange' + }); + frappe.utils.play_sound("error"); + } + + get_item_from_frm({ name, item_code, batch_no, uom, rate }) { + let item_row = null; + if (name) { + item_row = this.frm.doc.items.find(i => i.name == name); + } else { + // if item is clicked twice from item selector + // then "item_code, batch_no, uom, rate" will help in getting the exact item + // to increase the qty by one + const has_batch_no = batch_no; + item_row = this.frm.doc.items.find( + i => i.item_code === item_code + && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) + && (i.uom === uom) + && (i.rate == rate) + ); + } + + return item_row || {}; } edit_item_details_of(item_row) { @@ -585,9 +602,7 @@ erpnext.PointOfSale.Controller = class { } is_current_item_being_edited(item_row) { - const { item_code, batch_no } = this.item_details.current_item; - - return item_code !== item_row.item_code || batch_no != item_row.batch_no ? false : true; + return item_row.name == this.item_details.current_item.name; } update_cart_html(item_row, remove_item) { @@ -669,7 +684,7 @@ erpnext.PointOfSale.Controller = class { update_item_field(value, field_or_action) { if (field_or_action === 'checkout') { - this.item_details.toggle_item_details_section(undefined); + this.item_details.toggle_item_details_section(null); } else if (field_or_action === 'remove') { this.remove_item_from_cart(); } else { @@ -688,7 +703,7 @@ erpnext.PointOfSale.Controller = class { .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); - this.item_details.toggle_item_details_section(undefined); + this.item_details.toggle_item_details_section(null); frappe.dom.unfreeze(); }) .catch(e => console.log(e)); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index f5019f5083..9de7beff46 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -181,11 +181,8 @@ erpnext.PointOfSale.ItemCart = class { me.$totals_section.find(".edit-cart-btn").click(); } - const item_code = unescape($cart_item.attr('data-item-code')); - const batch_no = unescape($cart_item.attr('data-batch-no')); - const uom = unescape($cart_item.attr('data-uom')); - const rate = unescape($cart_item.attr('data-rate')); - me.events.cart_item_clicked(item_code, batch_no, uom, rate); + const item_row_name = unescape($cart_item.attr('data-row-name')); + me.events.cart_item_clicked({ name: item_row_name }); this.numpad_value = ''; }); @@ -521,25 +518,14 @@ erpnext.PointOfSale.ItemCart = class { } } - get_cart_item({ item_code, batch_no, uom, rate }) { - const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; - const item_code_attr = `[data-item-code="${escape(item_code)}"]`; - const uom_attr = `[data-uom="${escape(uom)}"]`; - const rate_attr = `[data-rate="${escape(rate)}"]`; - - const item_selector = batch_no ? - `.cart-item-wrapper${batch_attr}${uom_attr}${rate_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}${rate_attr}`; - + get_cart_item({ name }) { + const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; return this.$cart_items_wrapper.find(item_selector); } get_item_from_frm(item) { const doc = this.events.get_frm().doc; - const { item_code, batch_no, uom, rate } = item; - const search_field = batch_no ? 'batch_no' : 'item_code'; - const search_value = batch_no || item_code; - - return doc.items.find(i => i[search_field] === search_value && i.uom === uom && i.rate === rate); + return doc.items.find(i => i.name == item.name); } update_item_html(item, remove_item) { @@ -564,10 +550,7 @@ erpnext.PointOfSale.ItemCart = class { if (!$item_to_update.length) { this.$cart_items_wrapper.append( - `
    -
    + `
    ` ) $item_to_update = this.get_cart_item(item_data); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 5e09df8efe..43a29b9c75 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -54,36 +54,28 @@ erpnext.PointOfSale.ItemDetails = class { this.$dicount_section = this.$component.find('.discount-section'); } - has_item_has_changed(item) { - const { item_code, batch_no, uom, rate } = this.current_item; - const item_code_is_same = item && item_code === item.item_code; - const batch_is_same = item && batch_no == item.batch_no; - const uom_is_same = item && uom === item.uom; - const rate_is_same = item && rate === item.rate; - - if (!item) - return false; - - if (item_code_is_same && batch_is_same && uom_is_same && rate_is_same) - return false; - - return true; + compare_with_current_item(item) { + // returns true if `item` is currently being edited + return item && item.name == this.current_item.name } toggle_item_details_section(item) { - this.item_has_changed = this.has_item_has_changed(item); + const current_item_changed = !this.compare_with_current_item(item); - this.events.toggle_item_selector(this.item_has_changed); - this.toggle_component(this.item_has_changed); + // if item is null or highlighted cart item is clicked twice + const hide_item_details = !Boolean(item) || !current_item_changed; + + this.events.toggle_item_selector(!hide_item_details); + this.toggle_component(!hide_item_details); - if (this.item_has_changed) { + if (item && current_item_changed) { this.doctype = item.doctype; this.item_meta = frappe.get_meta(this.doctype); this.name = item.name; this.item_row = item; this.currency = this.events.get_frm().doc.currency; - this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom, rate: item.rate }; + this.current_item = item this.render_dom(item); this.render_discount_dom(item); @@ -180,7 +172,7 @@ erpnext.PointOfSale.ItemDetails = class { df: { ...field_meta, onchange: function() { - me.events.form_updated(me.doctype, me.name, fieldname, this.value); + me.events.form_updated(me.current_item, fieldname, this.value); } }, parent: this.$form_container.find(`.${fieldname}-control`), @@ -218,22 +210,17 @@ erpnext.PointOfSale.ItemDetails = class { bind_custom_control_change_event() { const me = this; if (this.rate_control) { - if (this.allow_rate_change) { - this.rate_control.df.onchange = function() { - if (this.value || flt(this.value) === 0) { - me.events.set_value_in_current_cart_item('rate', this.value); - me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => { - const item_row = frappe.get_doc(me.doctype, me.name); - const doc = me.events.get_frm().doc; - me.$item_price.html(format_currency(item_row.rate, doc.currency)); - me.render_discount_dom(item_row); - }); - me.current_item.rate = this.value; - } - }; - } else { - this.rate_control.df.read_only = 1; - } + this.rate_control.df.onchange = function() { + if (this.value || flt(this.value) === 0) { + me.events.form_updated(me.current_item, 'rate', this.value).then(() => { + const item_row = frappe.get_doc(me.doctype, me.name); + const doc = me.events.get_frm().doc; + me.$item_price.html(format_currency(item_row.rate, doc.currency)); + me.render_discount_dom(item_row); + }); + } + }; + this.rate_control.df.read_only = !this.allow_rate_change; this.rate_control.refresh(); } @@ -246,7 +233,7 @@ erpnext.PointOfSale.ItemDetails = class { this.warehouse_control.df.reqd = 1; this.warehouse_control.df.onchange = function() { if (this.value) { - me.events.form_updated(me.doctype, me.name, 'warehouse', this.value).then(() => { + me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { me.item_stock_map = me.events.get_item_stock_map(); const available_qty = me.item_stock_map[me.item_row.item_code][this.value]; if (available_qty === undefined) { @@ -278,7 +265,7 @@ erpnext.PointOfSale.ItemDetails = class { this.serial_no_control.df.reqd = 1; this.serial_no_control.df.onchange = async function() { !me.current_item.batch_no && await me.auto_update_batch_no(); - me.events.form_updated(me.doctype, me.name, 'serial_no', this.value); + me.events.form_updated(me.current_item, 'serial_no', this.value); } this.serial_no_control.refresh(); } @@ -295,19 +282,12 @@ erpnext.PointOfSale.ItemDetails = class { } } }; - this.batch_no_control.df.onchange = function() { - me.events.set_value_in_current_cart_item('batch-no', this.value); - me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); - me.current_item.batch_no = this.value; - } this.batch_no_control.refresh(); } if (this.uom_control) { this.uom_control.df.onchange = function() { - me.events.set_value_in_current_cart_item('uom', this.value); - me.events.form_updated(me.doctype, me.name, 'uom', this.value); - me.current_item.uom = this.value; + me.events.form_updated(me.current_item, 'uom', this.value); const item_row = frappe.get_doc(me.doctype, me.name); me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value); @@ -317,9 +297,9 @@ erpnext.PointOfSale.ItemDetails = class { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { const field_control = this[`${fieldname}_control`]; - const item_is_same = !this.has_item_has_changed(item_row); + const item_row_is_being_edited = this.compare_with_current_item(item_row); - if (item_is_same && field_control && field_control.get_value() !== value) { + if (item_row_is_being_edited && field_control && field_control.get_value() !== value) { field_control.set_value(value); cur_pos.update_cart_html(item_row); } @@ -337,7 +317,9 @@ erpnext.PointOfSale.ItemDetails = class { fields: ["batch_no", "name"] }); const batch_serial_map = serials_with_batch_no.reduce((acc, r) => { - acc[r.batch_no] || (acc[r.batch_no] = []); + if (!acc[r.batch_no]) { + acc[r.batch_no] = []; + } acc[r.batch_no] = [...acc[r.batch_no], r.name]; return acc; }, {}); @@ -353,12 +335,10 @@ erpnext.PointOfSale.ItemDetails = class { if (serial_nos_belongs_to_other_batch) { this.serial_no_control.set_value(batch_serial_nos); this.qty_control.set_value(batch_serial_map[batch_no].length); - } - delete batch_serial_map[batch_no]; - - if (serial_nos_belongs_to_other_batch) + delete batch_serial_map[batch_no]; this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); + } } } diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 64c529ee4a..dd7f143c4c 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -232,7 +232,11 @@ erpnext.PointOfSale.ItemSelector = class { uom = uom === "undefined" ? undefined : uom; rate = rate === "undefined" ? undefined : rate; - me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom, rate }}); + me.events.item_selected({ + field: 'qty', + value: "+1", + item: { item_code, batch_no, serial_no, uom, rate } + }); me.set_search_value(''); }); From ea70f6f933b4ef6c1d1ec257244d67320e85cea7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Jun 2021 21:18:44 +0530 Subject: [PATCH 215/550] fix: hide images from cart & details --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 2 +- erpnext/selling/page/point_of_sale/pos_item_details.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 9de7beff46..7cae0e4797 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class { function get_item_image_html() { const { image, item_name } = item_data; - if (image) { + if (!me.hide_images && image) { return `
    Date: Thu, 24 Jun 2021 14:29:22 +0530 Subject: [PATCH 216/550] fix: add missing semicolons --- erpnext/selling/page/point_of_sale/pos_controller.js | 5 ++--- erpnext/selling/page/point_of_sale/pos_item_details.js | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 4c938756c7..f5c5a0ae09 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -276,14 +276,13 @@ erpnext.PointOfSale.Controller = class { form_updated: (item, field, value) => { const item_row = frappe.model.get_doc(item.doctype, item.name); if (item_row && item_row[field] != value) { - const args = { field, value, item: this.item_details.current_item - } + }; return this.on_cart_update(args) - } + }; return Promise.resolve(); }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 637fb908a8..6a4d3d5214 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -57,7 +57,7 @@ erpnext.PointOfSale.ItemDetails = class { compare_with_current_item(item) { // returns true if `item` is currently being edited - return item && item.name == this.current_item.name + return item && item.name == this.current_item.name; } toggle_item_details_section(item) { @@ -76,7 +76,7 @@ erpnext.PointOfSale.ItemDetails = class { this.item_row = item; this.currency = this.events.get_frm().doc.currency; - this.current_item = item + this.current_item = item; this.render_dom(item); this.render_discount_dom(item); From 3b126014613d2ecdc97493b47985bbd628e78451 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Jun 2021 15:01:33 +0530 Subject: [PATCH 217/550] fix: sider issues --- erpnext/selling/page/point_of_sale/pos_controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index f5c5a0ae09..c827368dbf 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -281,8 +281,8 @@ erpnext.PointOfSale.Controller = class { value, item: this.item_details.current_item }; - return this.on_cart_update(args) - }; + return this.on_cart_update(args); + } return Promise.resolve(); }, From e21d44c5c3b0ba8ef2252aba8fb2714e078bb159 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 15:04:44 +0530 Subject: [PATCH 218/550] fix(Asset): Remove to_date field --- erpnext/assets/doctype/asset/asset.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index d77eb10418..de060757e2 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -53,7 +53,6 @@ "next_depreciation_date", "section_break_14", "schedules", - "to_date", "insurance_details", "policy_number", "insurer", @@ -481,12 +480,6 @@ "fieldname": "section_break_36", "fieldtype": "Section Break", "label": "Finance Books" - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 1, - "label": "To Date" } ], "idx": 72, @@ -509,7 +502,7 @@ "link_fieldname": "asset" } ], - "modified": "2021-06-19 13:56:58.450182", + "modified": "2021-06-24 14:58:51.097908", "modified_by": "Administrator", "module": "Assets", "name": "Asset", From dbdf2515cd92669ed2ed0c6b71302d5ee6ad89a3 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 23 Jun 2021 09:54:12 +0530 Subject: [PATCH 219/550] fix: fetches correct preferred shipping address --- erpnext/accounts/custom/address.py | 2 ++ erpnext/public/js/utils/party.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index 5e764037a7..c417a493c6 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -33,6 +33,8 @@ def get_shipping_address(company, address = None): if address and frappe.db.get_value('Dynamic Link', {'parent': address, 'link_name': company}): filters.append(["Address", "name", "=", address]) + if not address: + filters.append(["Address", "is_shipping_address", "=", 1]) address = frappe.get_all("Address", filters=filters, fields=fields) or {} diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 808dd5add0..99c8587391 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -274,9 +274,9 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { return true; } -erpnext.utils.get_shipping_address = function(frm, callback){ +erpnext.utils.get_shipping_address = function(frm, callback) { if (frm.doc.company) { - if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + if ((frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || frm.doc.internal_order_reference)) { if (callback) { return callback(); From da82bd4b51ff2670a5041bef3e28005adb39d2df Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 24 Jun 2021 17:23:21 +0530 Subject: [PATCH 220/550] refactor: update cost updates operation time and hour rates in BOM (#25891) * refactor: updates hour_rate and operation time on update cost * refactor: hour_rates are updated in routing when updated in workstations * test: test cases for updating hour_rates and operation time in linked bom --- erpnext/manufacturing/doctype/bom/bom.py | 45 +++++++++----- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../manufacturing/doctype/routing/routing.py | 14 ++++- .../doctype/routing/test_routing.py | 58 +++++++++++++++-- .../doctype/workstation/test_workstation.py | 62 ++++++++++++++++++- .../doctype/workstation/workstation.py | 5 +- 6 files changed, 157 insertions(+), 29 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d1f63854c7..3f109d91b5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -81,7 +81,7 @@ class BOM(WebsiteGenerator): self.validate_operations() self.calculate_cost() self.update_stock_qty() - self.update_cost(update_parent=False, from_child_bom=True, save=False) + self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -213,7 +213,7 @@ class BOM(WebsiteGenerator): return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) @frappe.whitelist() - def update_cost(self, update_parent=True, from_child_bom=False, save=True): + def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True): if self.docstatus == 2: return @@ -242,7 +242,7 @@ class BOM(WebsiteGenerator): if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True - self.calculate_cost() + self.calculate_cost(update_hour_rate) if save: self.db_update() @@ -403,32 +403,47 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self): + def calculate_cost(self, update_hour_rate = False): """Calculate bom totals""" - self.calculate_op_cost() + self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost() self.calculate_sm_cost() self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost - def calculate_op_cost(self): + def calculate_op_cost(self, update_hour_rate = False): """Update workstation rate and calculates totals""" self.operating_cost = 0 self.base_operating_cost = 0 for d in self.get('operations'): if d.workstation: - if not d.hour_rate: - hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate")) - d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate - - if d.hour_rate and d.time_in_mins: - d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate) - d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 - d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate) + self.update_rate_and_time(d, update_hour_rate) self.operating_cost += flt(d.operating_cost) self.base_operating_cost += flt(d.base_operating_cost) + def update_rate_and_time(self, row, update_hour_rate = False): + if not row.hour_rate or update_hour_rate: + hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) + row.hour_rate = (hour_rate / flt(self.conversion_rate) + if self.conversion_rate and hour_rate else hour_rate) + + if self.routing: + row.time_in_mins = flt(frappe.db.get_value("BOM Operation", { + "workstation": row.workstation, + "operation": row.operation, + "sequence_id": row.sequence_id, + "parent": self.routing + }, ["time_in_mins"])) + + if row.hour_rate and row.time_in_mins: + row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + + if update_hour_rate: + row.db_update() + def calculate_rm_cost(self): """Fetch RM rate as per today's valuation rate and calculate totals""" total_rm_cost = 0 @@ -975,7 +990,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - + return frappe.get_all("Item", fields = fields, filters=query_filters, or_filters = or_cond_filters, order_by=order_by, diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 42b23f223d..1f443fb95a 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -123,7 +123,7 @@ class TestBOM(unittest.TestCase): bom.items[0].conversion_factor = 5 bom.insert() - bom.update_cost() + bom.update_cost(update_hour_rate = False) # test amounts in selected currency self.assertEqual(bom.items[0].rate, 300) diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index 8312d7436c..ece0db717a 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -4,14 +4,24 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint +from frappe.utils import cint, flt from frappe import _ from frappe.model.document import Document class Routing(Document): def validate(self): + self.calculate_operating_cost() self.set_routing_id() + def on_update(self): + self.calculate_operating_cost() + + def calculate_operating_cost(self): + for operation in self.operations: + if not operation.hour_rate: + operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') + operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2) + def set_routing_id(self): sequence_id = 0 for row in self.operations: @@ -21,4 +31,4 @@ class Routing(Document): frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") .format(row.idx, row.sequence_id, sequence_id)) - sequence_id = row.sequence_id \ No newline at end of file + sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 6a38dcfa03..92f26946ab 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -7,9 +7,7 @@ import unittest import frappe from frappe.test_runner import make_test_records from erpnext.stock.doctype.item.test_item import make_item -from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError -from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record class TestRouting(unittest.TestCase): @@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase): wo_doc.cancel() wo_doc.delete() + def test_update_bom_operation_time(self): + operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "hour_rate_labour": 750 , + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation B", + "hour_rate_labour": 200, + "hour_rate_rent": 1000, + "time_in_mins": 20 + } + ] + + test_routing_operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "time_in_mins": 30 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "time_in_mins": 20 + } + ] + setup_operations(operations) + routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR') + self.assertEqual(routing_doc.operations[0].time_in_mins, 30) + self.assertEqual(routing_doc.operations[1].time_in_mins, 20) + routing_doc.operations[0].time_in_mins = 90 + routing_doc.operations[1].time_in_mins = 42.2 + routing_doc.save() + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(bom_doc.operations[0].time_in_mins, 90) + self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2) + + def setup_operations(rows): + from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + from erpnext.manufacturing.doctype.operation.test_operation import make_operation for row in rows: make_workstation(row) make_operation(row) @@ -61,12 +105,14 @@ def create_routing(**args): if not args.do_not_save: try: - for operation in args.operations: - doc.append("operations", operation) - doc.insert() except frappe.DuplicateEntryError: doc = frappe.get_doc("Routing", args.routing_name) + doc.delete_key('operations') + for operation in args.operations: + doc.append("operations", operation) + + doc.save() return doc @@ -91,7 +137,7 @@ def setup_bom(**args): name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name') if not name: bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"), - routing = args.routing, with_operations=1) + routing = args.routing, with_operations=1, currency = args.currency) else: bom_doc = frappe.get_doc("BOM", name) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index c6699bee48..9b73aca601 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -1,16 +1,19 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt from __future__ import unicode_literals +from erpnext.manufacturing.doctype.operation.test_operation import make_operation import frappe import unittest from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError +from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing +from frappe.test_runner import make_test_records test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') +make_test_records('Workstation') class TestWorkstation(unittest.TestCase): - def test_validate_timings(self): check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") @@ -21,6 +24,58 @@ class TestWorkstation(unittest.TestCase): self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + def test_update_bom_operation_rate(self): + operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate_rent": 300, + "time_in_mins": 60 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation B", + "hour_rate_rent": 1000, + "time_in_mins": 60 + } + ] + + for row in operations: + make_workstation(row) + make_operation(row) + + test_routing_operations = [ + { + "operation": "Test Operation A", + "workstation": "_Test Workstation A", + "time_in_mins": 60 + }, + { + "operation": "Test Operation B", + "workstation": "_Test Workstation A", + "time_in_mins": 60 + } + ] + routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations) + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") + w1 = frappe.get_doc("Workstation", "_Test Workstation A") + #resets values + w1.hour_rate_rent = 300 + w1.hour_rate_labour = 0 + w1.save() + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(w1.hour_rate, 300) + self.assertEqual(bom_doc.operations[0].hour_rate, 300) + w1.hour_rate_rent = 250 + w1.save() + #updating after setting new rates in workstations + bom_doc.update_cost() + bom_doc.reload() + self.assertEqual(w1.hour_rate, 250) + self.assertEqual(bom_doc.operations[0].hour_rate, 250) + self.assertEqual(bom_doc.operations[1].hour_rate, 250) + def make_workstation(*args, **kwargs): args = args if args else kwargs if isinstance(args, tuple): @@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs): "doctype": "Workstation", "workstation_name": workstation_name }) - + doc.hour_rate_rent = args.get("hour_rate_rent") + doc.hour_rate_labour = args.get("hour_rate_labour") doc.insert() return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Workstation", workstation_name) \ No newline at end of file + return frappe.get_doc("Workstation", workstation_name) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 3512e59045..f4483f7547 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -39,7 +39,8 @@ class Workstation(Document): def update_bom_operation(self): bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` - where workstation = %s""", self.name) + where workstation = %s and parenttype = 'routing' """, self.name) + for bom_no in bom_list: frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s where parent = %s and workstation = %s""", @@ -71,7 +72,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da def is_within_operating_hours(workstation, operation, from_datetime, to_datetime): operation_length = time_diff_in_seconds(to_datetime, from_datetime) workstation = frappe.get_doc("Workstation", workstation) - + if not workstation.working_hours: return From 755ebdf5828f31c0f8558bcceb20b4b1605586d7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 24 Jun 2021 17:35:14 +0530 Subject: [PATCH 221/550] Update party.js --- erpnext/public/js/utils/party.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 99c8587391..a79eadc761 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -276,7 +276,7 @@ erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { erpnext.utils.get_shipping_address = function(frm, callback) { if (frm.doc.company) { - if ((frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference || + if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference || frm.doc.internal_order_reference)) { if (callback) { return callback(); From 9dc625c1c9b841d950829e1b33024e596b742b45 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 24 Jun 2021 17:36:39 +0530 Subject: [PATCH 222/550] fix: validate product bundle for existing transactions before deletion (#25978) --- .../doctype/product_bundle/product_bundle.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index d3281f733f..ae3482f402 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -4,6 +4,8 @@ from __future__ import unicode_literals import frappe +from frappe.utils import get_link_to_form + from frappe import _ from frappe.model.document import Document @@ -18,6 +20,27 @@ class ProductBundle(Document): from erpnext.utilities.transaction_base import validate_uom_is_integer validate_uom_is_integer(self, "uom", "qty") + def on_trash(self): + linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice", + "Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"] + + invoice_links = [] + for doctype in linked_doctypes: + item_doctype = doctype + " Item" + + if doctype == "Stock Entry": + item_doctype = doctype + " Detail" + + invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"]) + + for invoice in invoices: + invoice_links.append(get_link_to_form(doctype, invoice['parent'])) + + if len(invoice_links): + frappe.throw( + "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" + .format(", ".join(invoice_links)), title=_("Not Allowed")) + def validate_main_item(self): """Validates, main Item is not a stock item""" if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"): From 1ca8f6a51d6ac6a374af3cf95a23b51d3e33f3ea Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 17 Jun 2021 13:05:43 +0530 Subject: [PATCH 223/550] fix: purchase receipt gl entries with same item code --- erpnext/accounts/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 59009ae621..25d2cf10bd 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None): def check_if_in_list(gle, gl_map, dimensions=None): account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type', - 'cost_center', 'project'] + 'cost_center', 'project', 'voucher_detail_no'] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions From f6dce4df73b2a61ca93e8c4119b2a2ce3ead39b6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Jun 2021 11:18:56 +0530 Subject: [PATCH 224/550] test: service item purchase with perpetual inventory enabled --- .../purchase_receipt/test_purchase_receipt.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 07c5da1dca..2eb8bfd5d2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1011,6 +1011,47 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(pr.status, "To Bill") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) + def test_service_item_purchase_with_perpetual_inventory(self): + company = '_Test Company with perpetual inventory' + service_item = '_Test Non Stock Item' + + before_test_value = frappe.db.get_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items') + frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', 1) + srbnb_account = 'Stock Received But Not Billed - TCP1' + frappe.db.set_value('Company', company, 'service_received_but_not_billed', srbnb_account) + + pr = make_purchase_receipt( + company=company, item=service_item, + warehouse='Finished Goods - TCP1', do_not_save=1 + ) + item_row_with_diff_rate = frappe.copy_doc(pr.items[0]) + item_row_with_diff_rate.rate = 100 + pr.append('items', item_row_with_diff_rate) + + pr.save() + pr.submit() + + item_one_gl_entry = frappe.db.get_all("GL Entry", { + 'voucher_type': pr.doctype, + 'voucher_no': pr.name, + 'account': srbnb_account, + 'voucher_detail_no': pr.items[0].name + }, pluck="name") + + item_two_gl_entry = frappe.db.get_all("GL Entry", { + 'voucher_type': pr.doctype, + 'voucher_no': pr.name, + 'account': srbnb_account, + 'voucher_detail_no': pr.items[1].name + }, pluck="name") + + # check if the entries are not merged into one + # seperate entries should be made since voucher_detail_no is different + self.assertEqual(len(item_one_gl_entry), 1) + self.assertEqual(len(item_two_gl_entry), 1) + + frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 6e8148909540f358f4cffc00dce29e3e70cf671d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 5 Jan 2021 14:18:26 +0530 Subject: [PATCH 225/550] feat: Job Card Enhancements --- .../doctype/bom_operation/bom_operation.json | 9 +- .../doctype/job_card/job_card.js | 136 +++++---- .../doctype/job_card/job_card.json | 64 ++-- .../doctype/job_card/job_card.py | 95 +++++- .../doctype/job_card_operation/__init__.py | 0 .../job_card_operation.json | 52 ++++ .../job_card_operation/job_card_operation.py | 10 + .../job_card_time_log/job_card_time_log.json | 24 +- .../manufacturing_settings.json | 19 +- .../doctype/operation/operation.js | 4 +- .../doctype/operation/operation.json | 274 ++++++++---------- .../doctype/operation/operation.py | 26 ++ .../doctype/sub_operation/__init__.py | 0 .../doctype/sub_operation/sub_operation.js | 8 + .../doctype/sub_operation/sub_operation.json | 51 ++++ .../doctype/sub_operation/sub_operation.py | 10 + .../sub_operation/test_sub_operation.py | 10 + .../doctype/work_order/work_order.js | 3 +- .../doctype/work_order/work_order.json | 55 ++++ .../doctype/work_order/work_order.py | 134 +++++++-- .../doctype/work_order_batch/__init__.py | 0 .../work_order_batch/work_order_batch.json | 49 ++++ .../work_order_batch/work_order_batch.py | 10 + erpnext/stock/doctype/batch/batch.py | 9 +- .../stock/doctype/stock_entry/stock_entry.py | 81 +++++- 25 files changed, 834 insertions(+), 299 deletions(-) create mode 100644 erpnext/manufacturing/doctype/job_card_operation/__init__.py create mode 100644 erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json create mode 100644 erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py create mode 100644 erpnext/manufacturing/doctype/sub_operation/__init__.py create mode 100644 erpnext/manufacturing/doctype/sub_operation/sub_operation.js create mode 100644 erpnext/manufacturing/doctype/sub_operation/sub_operation.json create mode 100644 erpnext/manufacturing/doctype/sub_operation/sub_operation.py create mode 100644 erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py create mode 100644 erpnext/manufacturing/doctype/work_order_batch/__init__.py create mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json create mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 07464e3e76..57062b8ca4 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -13,10 +13,10 @@ "col_break1", "hour_rate", "time_in_mins", - "batch_size", "operating_cost", "base_hour_rate", "base_operating_cost", + "batch_size", "image" ], "fields": [ @@ -61,6 +61,8 @@ }, { "description": "In minutes", + "fetch_from": "operation.total_operation_time", + "fetch_if_empty": 1, "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 1, @@ -104,7 +106,8 @@ "label": "Image" }, { - "default": "1", + "fetch_from": "operation.batch_size", + "fetch_if_empty": 1, "fieldname": "batch_size", "fieldtype": "Int", "label": "Batch Size" @@ -120,7 +123,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-13 18:14:10.018774", + "modified": "2020-12-14 15:01:33.142869", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 4e8dd41022..57ec20b42c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', { } }; }); + + frm.set_indicator_formatter('sub_operation', + function(doc) { + if (doc.status == "Pending") { + return "red"; + } else { + return doc.status === "Complete" ? "green" : "orange"; + } + } + ); }, refresh: function(frm) { @@ -97,81 +107,76 @@ frappe.ui.form.on('Job Card', { prepare_timer_buttons: function(frm) { frm.trigger("make_dashboard"); - if (!frm.doc.job_started) { - frm.add_custom_button(__("Start"), () => { - if (!frm.doc.employee) { - frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee", - fieldname: 'employee'}, d => { - if (d.employee) { - frm.set_value("employee", d.employee); - } else { - frm.events.start_job(frm); - } - }, __("Enter Value"), __("Start")); - } else { - frm.events.start_job(frm); - } + + if (!frm.doc.started_time && !frm.doc.current_time) { + frm.add_custom_button(__("Start Job"), () => { + frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Employee'), options: "Job Card Time Log", + fieldname: 'employee'}, d => { + debugger + frm.events.start_job(frm, "Work In Progress", d.employee); + }, __("Assign Job to Employee")); }).addClass("btn-primary"); } else if (frm.doc.status == "On Hold") { - frm.add_custom_button(__("Resume"), () => { - frappe.flags.resume_job = 1; - frm.events.start_job(frm); + frm.add_custom_button(__("Resume Job"), () => { + frm.events.start_job(frm, "Resume Job"); }).addClass("btn-primary"); } else { - frm.add_custom_button(__("Pause"), () => { - frappe.flags.pause_job = 1; - frm.set_value("status", "On Hold"); - frm.events.complete_job(frm); + frm.add_custom_button(__("Pause Job"), () => { + frm.events.complete_job(frm, "On Hold"); }); - frm.add_custom_button(__("Complete"), () => { - let completed_time = frappe.datetime.now_datetime(); - frm.trigger("hide_timer"); - - if (frm.doc.for_quantity) { - frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), - fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => { - frm.events.complete_job(frm, completed_time, data.qty); - }, __("Enter Value"), __("Complete")); - } else { - frm.events.complete_job(frm, completed_time, 0); - } + frm.add_custom_button(__("Complete Job"), () => { + frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), + fieldname: 'qty', default: frm.doc.for_quantity}, data => { + frm.events.complete_job(frm, "Complete", data.qty); + }, __("Enter Value")); }).addClass("btn-primary"); } }, - start_job: function(frm) { - let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs'); - row.from_time = frappe.datetime.now_datetime(); - frm.set_value('job_started', 1); - frm.set_value('started_time' , row.from_time); - frm.set_value("status", "Work In Progress"); - - if (!frappe.flags.resume_job) { - frm.set_value('current_time' , 0); - } - - frm.save(); + start_job: function(frm, status, employee) { + const args = { + job_card_id: frm.doc.name, + start_time: frappe.datetime.now_datetime(), + employee: employee, + status: status + }; + frm.events.make_time_log(frm, args); }, - complete_job: function(frm, completed_time, completed_qty) { - frm.doc.time_logs.forEach(d => { - if (d.from_time && !d.to_time) { - d.to_time = completed_time || frappe.datetime.now_datetime(); - d.completed_qty = completed_qty || 0; + complete_job: function(frm, status, completed_qty) { + const args = { + job_card_id: frm.doc.name, + complete_time: frappe.datetime.now_datetime(), + status: status, + completed_qty: completed_qty + }; + frm.events.make_time_log(frm, args); + }, - if(frappe.flags.pause_job) { - let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0; - frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0)); - } else { - frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.set_value('current_time' , 0); - } + make_time_log: function(frm, args) { + frm.events.update_sub_operation(frm, args); - frm.save(); + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log", + args: { + args: args + }, + freeze: true, + callback: function (r) { + frm.reload_doc(); + frm.trigger("make_dashboard"); } - }); + }) + }, + + update_sub_operation: function(frm, args) { + if (frm.doc.sub_operations && frm.doc.sub_operations.length) { + let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete'); + if (sub_operations && sub_operations.length) { + args["sub_operation"] = sub_operations[0].sub_operation; + } + } }, validate: function(frm) { @@ -180,18 +185,8 @@ frappe.ui.form.on('Job Card', { } }, - employee: function(frm) { - if (frm.doc.job_started && !frm.doc.current_time) { - frm.trigger("reset_timer"); - } else { - frm.events.start_job(frm); - } - }, - reset_timer: function(frm) { frm.set_value('started_time' , ''); - frm.set_value('job_started', 0); - frm.set_value('current_time' , 0); }, make_dashboard: function(frm) { @@ -297,7 +292,6 @@ frappe.ui.form.on('Job Card Time Log', { }, to_time: function(frm) { - frm.set_value('job_started', 0); frm.set_value('started_time', ''); } }) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 5713f697e9..c2fd8cc3f9 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -9,30 +9,30 @@ "naming_series", "work_order", "bom_no", - "workstation", - "operation", - "operation_row_number", "column_break_4", "posting_date", "company", - "remarks", "production_section", "production_item", "item_name", "for_quantity", - "quality_inspection", - "wip_warehouse", "column_break_12", - "employee", - "employee_name", - "status", + "wip_warehouse", + "quality_inspection", "project", + "operation_section_section", + "operation", + "operation_row_number", + "column_break_18", + "workstation", + "section_break_21", + "sub_operations", "timing_detail", "time_logs", "section_break_13", "total_completed_qty", - "total_time_in_mins", "column_break_15", + "total_time_in_mins", "section_break_8", "items", "more_information", @@ -40,7 +40,9 @@ "sequence_id", "transferred_qty", "requested_qty", + "status", "column_break_20", + "remarks", "barcode", "job_started", "started_time", @@ -117,13 +119,6 @@ "fieldtype": "Section Break", "label": "Timing Detail" }, - { - "fieldname": "employee", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "Employee", - "options": "Employee" - }, { "allow_bulk_edit": 1, "fieldname": "time_logs", @@ -133,9 +128,11 @@ }, { "fieldname": "section_break_13", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { + "default": "0", "fieldname": "total_completed_qty", "fieldtype": "Float", "label": "Total Completed Qty", @@ -251,12 +248,7 @@ "reqd": 1 }, { - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Read Only", - "label": "Employee Name" - }, - { + "collapsible": 1, "fieldname": "production_section", "fieldtype": "Section Break", "label": "Production" @@ -314,11 +306,33 @@ "label": "Quality Inspection", "no_copy": 1, "options": "Quality Inspection" + }, + { + "allow_bulk_edit": 1, + "fieldname": "sub_operations", + "fieldtype": "Table", + "label": "Sub Operations", + "options": "Job Card Operation", + "read_only": 1 + }, + { + "fieldname": "operation_section_section", + "fieldtype": "Section Break", + "label": "Operation Section" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_21", + "fieldtype": "Section Break", + "hide_border": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-11-19 18:26:50.531664", + "modified": "2020-12-14 15:14:05.566271", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index cdc4518894..5c157d43ec 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -4,12 +4,13 @@ from __future__ import unicode_literals import frappe -import datetime +import datetime, json from frappe import _, bold +from six import string_types from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, - get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form) + get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds) from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations @@ -25,9 +26,20 @@ class JobCard(Document): self.set_status() self.validate_operation_id() self.validate_sequence_id() + self.get_sub_operations() + self.update_sub_operation_status() + + def get_sub_operations(self): + if self.operation: + self.sub_operations = [] + for row in frappe.get_all("Sub Operation", + filters = {"parent": self.operation}, fields=["operation"]): + self.append("sub_operations", { + "sub_operation": row.operation, + "status": "Pending" + }) def validate_time_logs(self): - self.total_completed_qty = 0.0 self.total_time_in_mins = 0.0 if self.get('time_logs'): @@ -46,6 +58,8 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty + else: + self.total_completed_qty = 0.0 self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) @@ -57,7 +71,7 @@ class JobCard(Document): self.workstation, 'production_capacity') or 1 validate_overlap_for = " and jc.workstation = %(workstation)s " - if self.employee: + if args.get("employee"): # override capacity for employee production_capacity = 1 validate_overlap_for = " and jc.employee = %(employee)s " @@ -80,7 +94,7 @@ class JobCard(Document): "to_time": args.to_time, "name": args.name or "No Name", "parent": args.parent or "No Name", - "employee": self.employee, + "employee": args.get("employee"), "workstation": self.workstation }, as_dict=True) @@ -158,6 +172,66 @@ class JobCard(Document): row.planned_start_time = datetime.datetime.combine(start_date, get_time(workstation_doc.working_hours[0].start_time)) + def add_time_log(self, args): + last_row = [] + if self.time_logs and len(self.time_logs) > 0: + last_row = self.time_logs[-1] + + self.reset_timer_value(args) + if last_row and args.get("complete_time"): + last_row.update({ + "to_time": get_datetime(args.get("complete_time")), + "operation": args.get("sub_operation"), + "completed_qty": args.get("completed_qty") or 0.0 + }) + elif args.get("start_time"): + self.append("time_logs", { + "from_time": get_datetime(args.get("start_time")), + "employee": args.get("employee"), + "operation": args.get("sub_operation"), + "completed_qty": 0.0 + }) + + if self.status == "On Hold": + self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) + + self.save() + + def reset_timer_value(self, args): + self.started_time = None + + if args.get("status") in ["Work In Progress", "Complete"]: + self.current_time = 0.0 + + if args.get("status") == "Work In Progress": + self.started_time = get_datetime(args.get("start_time")) + + if args.get("status") == "Resume Job": + args["status"] = "Work In Progress" + + if args.get("status"): + self.status = args.get("status") + + def update_sub_operation_status(self): + if not (self.sub_operations and self.time_logs): return + + operation_wise_completed_time = {} + for time_log in self.time_logs: + if time_log.operation not in operation_wise_completed_time: + operation_wise_completed_time.setdefault(time_log.operation, + frappe._dict({"status": "Pending", "completed_time": 0.0})) + + op_row = operation_wise_completed_time[time_log.operation] + op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" + if time_log.time_in_mins: + op_row.completed_time += time_log.time_in_mins + + for row in self.sub_operations: + operation_deatils = operation_wise_completed_time.get(row.sub_operation) + if operation_deatils: + row.status = operation_deatils.status + row.completed_time = operation_deatils.completed_time + def update_time_logs(self, row): self.append("time_logs", { "from_time": row.planned_start_time, @@ -376,6 +450,17 @@ class JobCard(Document): frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) + +@frappe.whitelist() +def make_time_log(args): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + doc = frappe.get_doc("Job Card", args.job_card_id) + doc.validate_sequence_id() + doc.add_time_log(args) + @frappe.whitelist() def get_operation_details(work_order, operation): if work_order and operation: diff --git a/erpnext/manufacturing/doctype/job_card_operation/__init__.py b/erpnext/manufacturing/doctype/job_card_operation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json new file mode 100644 index 0000000000..be8190236d --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "creation": "2020-12-07 16:58:38.449041", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sub_operation", + "completed_time", + "status" + ], + "fields": [ + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Complete\nPause\nPending\nWork In Progress", + "read_only": 1 + }, + { + "description": "In mins", + "fieldname": "completed_time", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Completed Time", + "read_only": 1 + }, + { + "fieldname": "sub_operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-14 17:08:25.992957", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Operation", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py new file mode 100644 index 0000000000..85d72982ed --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class JobCardOperation(Document): + pass diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json index 9dd54dd618..a7102d7d23 100644 --- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json +++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json @@ -1,14 +1,17 @@ { + "actions": [], "creation": "2019-03-08 23:56:43.187569", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "employee", "from_time", "to_time", "column_break_2", "time_in_mins", - "completed_qty" + "completed_qty", + "operation" ], "fields": [ { @@ -41,10 +44,27 @@ "in_list_view": 1, "label": "Completed Qty", "reqd": 1 + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee" + }, + { + "fieldname": "operation", + "fieldtype": "Link", + "label": "Operation", + "no_copy": 1, + "options": "Operation", + "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-12-03 12:56:02.285448", + "links": [], + "modified": "2020-12-23 14:30:00.970916", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Time Log", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index b7634da87c..6647be54eb 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -26,7 +26,9 @@ "column_break_16", "overproduction_percentage_for_work_order", "other_settings_section", - "update_bom_costs_automatically" + "update_bom_costs_automatically", + "column_break_23", + "make_serial_no_batch_from_work_order" ], "fields": [ { @@ -155,13 +157,24 @@ { "fieldname": "column_break_5", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order", + "fieldname": "make_serial_no_batch_from_work_order", + "fieldtype": "Check", + "label": "Make Serial No / Batch from Work Order" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 10:55:43.996581", + "modified": "2020-12-08 13:37:40.325838", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", @@ -178,4 +191,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js index 5c2aba6f09..9bfcc6eedb 100644 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ b/erpnext/manufacturing/doctype/operation/operation.js @@ -2,7 +2,5 @@ // For license information, please see license.txt frappe.ui.form.on('Operation', { - refresh: function(frm) { - } -}); +}); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json index c231fba2fa..9e6f8e1f5d 100644 --- a/erpnext/manufacturing/doctype/operation/operation.json +++ b/erpnext/manufacturing/doctype/operation/operation.json @@ -1,167 +1,133 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2014-11-07 16:20:30.683186", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2014-11-07 16:20:30.683186", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "workstation", + "data_2", + "cost_of_poor_quality_operation", + "job_card_section", + "create_job_card_based_on_batch_size", + "column_break_6", + "batch_size", + "sub_operations_section", + "sub_operations", + "total_operation_time", + "section_break_4", + "description" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "workstation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Default Workstation", - "length": 0, - "no_copy": 0, - "options": "Workstation", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Default Workstation", + "options": "Workstation" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Operation Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + }, + { + "collapsible": 1, + "fieldname": "sub_operations_section", + "fieldtype": "Section Break", + "label": "Sub Operations" + }, + { + "fieldname": "sub_operations", + "fieldtype": "Table", + "options": "Sub Operation" + }, + { + "description": "Time in mins.", + "fieldname": "total_operation_time", + "fieldtype": "Float", + "label": "Total Operation Time", + "read_only": 1 + }, + { + "fieldname": "data_2", + "fieldtype": "Column Break" + }, + { + "default": "1", + "depends_on": "create_job_card_based_on_batch_size", + "fieldname": "batch_size", + "fieldtype": "Int", + "label": "Batch Size", + "mandatory_depends_on": "create_job_card_based_on_batch_size" + }, + { + "default": "0", + "fieldname": "create_job_card_based_on_batch_size", + "fieldtype": "Check", + "label": "Create Job Card based on Batch Size" + }, + { + "default": "0", + "description": "Cost of poor quality operation", + "fieldname": "cost_of_poor_quality_operation", + "fieldtype": "Check", + "label": "Is COPQ Operation" + }, + { + "collapsible": 1, + "fieldname": "job_card_section", + "fieldtype": "Section Break", + "label": "Job Card" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-wrench", - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:28:27.462413", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Operation", - "name_case": "", - "owner": "Administrator", + ], + "icon": "fa fa-wrench", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-24 14:25:03.428303", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Operation", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "read": 1, + "role": "Manufacturing User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "Manufacturing Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "export": 1, + "import": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py index 69e83292ff..aaf0d5c01b 100644 --- a/erpnext/manufacturing/doctype/operation/operation.py +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -2,9 +2,35 @@ # For license information, please see license.txt from __future__ import unicode_literals + +import frappe +from frappe import _ +from frappe.utils import flt from frappe.model.document import Document class Operation(Document): def validate(self): if not self.description: self.description = self.name + + self.duplicate_sub_operation() + self.set_total_time() + + def duplicate_sub_operation(self): + operation_list = [] + for row in self.sub_operations: + if row.operation in operation_list: + frappe.throw(_("The operation {0} can not add multiple times") + .format(frappe.bold(row.operation))) + + if self.name == row.operation: + frappe.throw(_("The operation {0} can not be the sub operation") + .format(frappe.bold(row.operation))) + + operation_list.append(row.operation) + + def set_total_time(self): + self.total_operation_time = 0.0 + + for row in self.sub_operations: + self.total_operation_time += row.time_in_mins diff --git a/erpnext/manufacturing/doctype/sub_operation/__init__.py b/erpnext/manufacturing/doctype/sub_operation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js new file mode 100644 index 0000000000..be9db6a408 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Sub Operation', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json new file mode 100644 index 0000000000..f63d2b9864 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "creation": "2020-12-07 15:39:47.488519", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "operation", + "time_in_mins", + "column_break_5", + "description" + ], + "fields": [ + { + "fieldname": "operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation" + }, + { + "description": "Time in mins", + "fieldname": "time_in_mins", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Operation Time" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-07 18:09:18.005578", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Sub Operation", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py new file mode 100644 index 0000000000..f4b27758e9 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class SubOperation(Document): + pass diff --git a/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py new file mode 100644 index 0000000000..d3410ca312 --- /dev/null +++ b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSubOperation(unittest.TestCase): + pass diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 8088d930df..601734914d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -141,8 +141,7 @@ frappe.ui.form.on("Work Order", { } if (frm.doc.docstatus === 1 - && frm.doc.operations && frm.doc.operations.length - && frm.doc.qty != frm.doc.material_transferred_for_manufacturing) { + && frm.doc.operations && frm.doc.operations.length) { const not_completed = frm.doc.operations.filter(d => { if(d.status != 'Completed') { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index cd9edeeea8..cb3c942107 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -21,6 +21,13 @@ "produced_qty", "sales_order", "project", + "serial_no_and_batch_for_finished_good_section", + "has_serial_no", + "has_batch_no", + "column_break_17", + "serial_no", + "batch_size", + "batches", "settings_section", "allow_alternative_item", "use_multi_level_bom", @@ -488,6 +495,54 @@ "fieldtype": "Float", "label": "Lead Time", "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "eval:!doc.__islocal", + "fieldname": "serial_no_and_batch_for_finished_good_section", + "fieldtype": "Section Break", + "label": "Serial No and Batch for Finished Good" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "production_item.has_serial_no", + "fieldname": "has_serial_no", + "fieldtype": "Check", + "label": "Has Serial No", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "production_item.has_batch_no", + "fieldname": "has_batch_no", + "fieldtype": "Check", + "label": "Has Batch No", + "read_only": 1 + }, + { + "depends_on": "has_serial_no", + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial Nos" + }, + { + "default": "0", + "depends_on": "has_batch_no", + "fieldname": "batch_size", + "fieldtype": "Float", + "label": "Batch Size" + }, + { + "depends_on": "has_batch_no", + "fieldname": "batches", + "fieldtype": "Table", + "label": "Batches", + "options": "Work Order Batch", + "read_only": 1 } ], "icon": "fa fa-cogs", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2600790a59..587204c341 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -19,6 +19,8 @@ from frappe.utils.csvutils import getlink from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty from erpnext.utilities.transaction_base import validate_uom_is_integer from frappe.model.mapper import get_mapped_doc +from erpnext.stock.doctype.batch.batch import make_batch +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos class OverProductionError(frappe.ValidationError): pass class CapacityError(frappe.ValidationError): pass @@ -40,6 +42,7 @@ class WorkOrder(Document): self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order) def validate(self): + self.set("batches", []) self.validate_production_item() if self.bom_no: validate_bom_no(self.production_item, self.bom_no) @@ -235,6 +238,9 @@ class WorkOrder(Document): production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) + def before_submit(self): + self.create_serial_no_batch_no() + def on_submit(self): if not self.wip_warehouse: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) @@ -266,6 +272,67 @@ class WorkOrder(Document): self.update_planned_qty() self.update_ordered_qty() self.update_reserved_qty_for_production() + self.delete_auto_created_batch_and_serial_no() + + def create_serial_no_batch_no(self): + if not (self.has_serial_no or self.has_batch_no): return + + if not cint(frappe.db.get_single_value("Manufacturing Settings", + "make_serial_no_batch_from_work_order")): return + + if self.has_batch_no: + self.set("batches", []) + self.create_batch_for_finished_good() + + args = {"item_code": self.production_item} + + if self.has_serial_no: + self.make_serial_nos(args) + + def create_batch_for_finished_good(self): + total_qty = self.qty + if not self.batch_size: + self.batch_size = total_qty + + while total_qty > 0: + qty = self.batch_size + if self.batch_size >= total_qty: + qty = total_qty + + if total_qty > self.batch_size: + total_qty -= self.batch_size + else: + qty = total_qty + total_qty = 0 + + batch = make_batch(self.production_item) + self.append("batches", { + "batch_no": batch, + "qty": qty, + }) + + def delete_auto_created_batch_and_serial_no(self): + if self.serial_no: + for d in get_serial_nos(self.serial_no): + frappe.delete_doc("Serial No", d) + + for row in self.batches: + batch_no = row.batch_no + row.db_set("batch_no", None) + frappe.delete_doc("Batch", batch_no) + + def make_serial_nos(self, args): + serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") + if serial_no_series: + self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) + elif self.serial_no: + args.update({"serial_no": self.serial_no, "actual_qty": self.qty, "batch_no": self.batch_no}) + self.serial_no = auto_make_serial_nos(args) + + serial_nos_length = len(get_serial_nos(self.serial_no)) + if serial_nos_length != self.qty: + frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.") + .format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError) def create_job_card(self): manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") @@ -273,32 +340,51 @@ class WorkOrder(Document): enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 - for i, row in enumerate(self.operations): - self.set_operation_start_end_time(i, row) + for index, row in enumerate(self.operations): + qty = self.qty + i=0 + while qty > 0: + i += 1 + if not cint(frappe.db.get_value("Operation", + row.operation, "create_job_card_based_on_batch_size")): + row.batch_size = self.qty - if not row.workstation: - frappe.throw(_("Row {0}: select the workstation against the operation {1}") - .format(row.idx, row.operation)) + job_card_qty = row.batch_size + if row.batch_size and qty >= row.batch_size: + qty -= row.batch_size + elif qty > 0: + job_card_qty = qty - original_start_time = row.planned_start_time - job_card_doc = create_job_card(self, row, - enable_capacity_planning=enable_capacity_planning, auto_create=True) - - if enable_capacity_planning and job_card_doc: - row.planned_start_time = job_card_doc.time_logs[-1].from_time - row.planned_end_time = job_card_doc.time_logs[-1].to_time - - if date_diff(row.planned_start_time, original_start_time) > plan_days: - frappe.message_log.pop() - frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") - .format(plan_days, row.operation), CapacityError) - - row.db_update() + if job_card_qty > 0: + self.prepare_data_for_job_card(row, job_card_qty, index, + plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: self.db_set("planned_end_date", planned_end_date) + def prepare_data_for_job_card(self, row, job_card_qty, index, plan_days, enable_capacity_planning): + self.set_operation_start_end_time(index, row) + + if not row.workstation: + frappe.throw(_("Row {0}: select the workstation against the operation {1}") + .format(row.idx, row.operation)) + + original_start_time = row.planned_start_time + job_card_doc = create_job_card(self, row, qty=job_card_qty, + enable_capacity_planning=enable_capacity_planning, auto_create=True) + + if enable_capacity_planning and job_card_doc: + row.planned_start_time = job_card_doc.time_logs[-1].from_time + row.planned_end_time = job_card_doc.time_logs[-1].to_time + + if date_diff(row.planned_start_time, original_start_time) > plan_days: + frappe.message_log.pop() + frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") + .format(plan_days, row.operation), CapacityError) + + row.db_update() + def set_operation_start_end_time(self, idx, row): """Set start and end time for given operation. If first operation, set start as `planned_start_date`, else add time diff to end time of earlier operation.""" @@ -669,6 +755,15 @@ class WorkOrder(Document): bom.set_bom_material_details() return bom + def update_batch_qty(self): + if self.has_batch_no and self.batches: + for row in self.batches: + qty = frappe.get_all("Stock Entry Detail", fields = ["sum(transfer_qty)"], + filters = {"docstatus": 1, "batch_no": row.batch_no, "is_finished_item": 1}, as_list=1) + + if qty: + frappe.db.set_value("Work Order Batch", row.name, "produced_qty", flt(qty[0][0])) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): @@ -826,6 +921,7 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.set_stock_entry_type() stock_entry.get_items() + stock_entry.set_serial_no_batch_for_finished_good() return stock_entry.as_dict() @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/work_order_batch/__init__.py b/erpnext/manufacturing/doctype/work_order_batch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json new file mode 100644 index 0000000000..ad667b7c39 --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "creation": "2021-01-04 16:42:39.347528", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "batch_no", + "qty", + "produced_qty" + ], + "fields": [ + { + "fieldname": "batch_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Batch No", + "options": "Batch" + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "default": "0", + "fieldname": "produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "no_copy": 1, + "print_hide": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-01-05 10:57:07.278399", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Batch", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py new file mode 100644 index 0000000000..cf3ec475ca --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WorkOrderBatch(Document): + pass diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 508e17c340..07cf08a5bb 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -308,4 +308,11 @@ def validate_serial_no_with_batch(serial_nos, item_code): message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" frappe.throw(_("There is no batch found against the {0}: {1}") - .format(message, serial_no_link)) \ No newline at end of file + .format(message, serial_no_link)) + +def make_batch(item_code): + if frappe.db.get_value("Item", item_code, "has_batch_no"): + doc = frappe.new_doc("Batch") + doc.item = item_code + doc.save() + return doc.name \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 66f8b63cb9..5fde35a811 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -498,6 +498,7 @@ class StockEntry(StockController): d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) if not d.t_warehouse: outgoing_items_cost += flt(d.basic_amount) + return outgoing_items_cost def get_args_for_incoming_rate(self, item): @@ -854,6 +855,7 @@ class StockEntry(StockController): pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") + pro_doc.update_batch_qty() if not pro_doc.operations: pro_doc.set_actual_dates() @@ -1076,18 +1078,45 @@ class StockEntry(StockController): # in case of BOM to_warehouse = item.get("default_warehouse") + args = { + "to_warehouse": to_warehouse, + "from_warehouse": "", + "qty": self.fg_completed_qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item.stock_uom, + "expense_account": item.get("expense_account"), + "cost_center": item.get("buying_cost_center"), + "is_finished_item": 1 + } + + if self.work_order and self.pro_doc.batches: + self.set_batchwise_finished_goods(args, item) + else: + self.add_finisged_goods(args, item) + + def set_batchwise_finished_goods(self, args, item): + qty = flt(self.fg_completed_qty) + for row in self.pro_doc.batches: + batch_qty = flt(row.qty) - flt(row.produced_qty) + if not batch_qty: continue + + if qty <=0: + break + + fg_qty = batch_qty + if batch_qty >= qty: + fg_qty = qty + + qty -= batch_qty + args["qty"] = fg_qty + args["batch_no"] = row.batch_no + + self.add_finisged_goods(args, item) + + def add_finisged_goods(self, args, item): self.add_to_stock_entry_detail({ - item.name: { - "to_warehouse": to_warehouse, - "from_warehouse": "", - "qty": self.fg_completed_qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item.stock_uom, - "expense_account": item.get("expense_account"), - "cost_center": item.get("buying_cost_center"), - "is_finished_item": 1 - } + item.name: args }, bom_no = self.bom_no) def get_bom_raw_materials(self, qty): @@ -1524,6 +1553,36 @@ class StockEntry(StockController): material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + def set_serial_no_batch_for_finished_good(self): + args = {} + if self.pro_doc.serial_no or self.pro_doc.batch_no: + self.get_serial_nos_for_fg(args) + + for row in self.items: + if row.is_finished_item and row.item_code == self.pro_doc.production_item: + if args.get("serial_no"): + row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)]) + + def get_serial_nos_for_fg(self, args): + fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", + "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"] + + filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"], + ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]] + + stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) + + if self.pro_doc.serial_no: + args["serial_no"] = self.get_available_serial_nos(stock_entries) + + def get_available_serial_nos(self, stock_entries): + used_serial_nos = [] + for row in stock_entries: + if row.serial_no: + used_serial_nos.extend(get_serial_nos(row.serial_no)) + + return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): From fcab53b238d2f6c1e0587ed309bf2765f63c72ec Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 5 Jan 2021 15:55:09 +0530 Subject: [PATCH 226/550] fix: skip job card --- .../doctype/bom_operation/bom_operation.json | 10 +++- .../doctype/work_order/work_order.js | 16 +++--- .../doctype/work_order/work_order.json | 9 --- .../doctype/work_order/work_order.py | 57 ++++++++++--------- .../work_order/work_order_dashboard.py | 7 +++ .../doctype/work_order_batch/__init__.py | 0 .../work_order_batch/work_order_batch.json | 49 ---------------- .../work_order_batch/work_order_batch.py | 10 ---- .../work_order_operation.json | 17 +++++- erpnext/stock/doctype/batch/batch.json | 31 +++++++++- erpnext/stock/doctype/batch/batch.py | 10 ++-- .../stock/doctype/serial_no/serial_no.json | 11 +++- erpnext/stock/doctype/serial_no/serial_no.py | 17 +++--- .../stock/doctype/stock_entry/stock_entry.py | 17 ++++-- 14 files changed, 132 insertions(+), 129 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/work_order_batch/__init__.py delete mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json delete mode 100644 erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 57062b8ca4..1330636198 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -11,6 +11,7 @@ "workstation", "description", "col_break1", + "skip_job_card", "hour_rate", "time_in_mins", "operating_cost", @@ -117,13 +118,20 @@ "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "skip_job_card", + "fieldtype": "Check", + "label": "Skip Job Card" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-14 15:01:33.142869", + "modified": "2021-01-05 14:29:11.887888", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 601734914d..adf6453e2e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -242,13 +242,15 @@ frappe.ui.form.on("Work Order", { if(data.completed_qty != frm.doc.qty) { pending_qty = frm.doc.qty - flt(data.completed_qty); - dialog.fields_dict.operations.df.data.push({ - 'name': data.name, - 'operation': data.operation, - 'workstation': data.workstation, - 'qty': pending_qty, - 'pending_qty': pending_qty, - }); + if (pending_qty && !data.skip_job_card) { + dialog.fields_dict.operations.df.data.push({ + 'name': data.name, + 'operation': data.operation, + 'workstation': data.workstation, + 'qty': pending_qty, + 'pending_qty': pending_qty, + }); + } } }); dialog.fields_dict.operations.grid.refresh(); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index cb3c942107..c80decb92e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -27,7 +27,6 @@ "column_break_17", "serial_no", "batch_size", - "batches", "settings_section", "allow_alternative_item", "use_multi_level_bom", @@ -535,14 +534,6 @@ "fieldname": "batch_size", "fieldtype": "Float", "label": "Batch Size" - }, - { - "depends_on": "has_batch_no", - "fieldname": "batches", - "fieldtype": "Table", - "label": "Batches", - "options": "Work Order Batch", - "read_only": 1 } ], "icon": "fa fa-cogs", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 587204c341..23cc090427 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -27,6 +27,7 @@ class CapacityError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass +class SerialNoQtyError(frappe.ValidationError): pass from six import string_types @@ -42,7 +43,6 @@ class WorkOrder(Document): self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order) def validate(self): - self.set("batches", []) self.validate_production_item() if self.bom_no: validate_bom_no(self.production_item, self.bom_no) @@ -281,10 +281,12 @@ class WorkOrder(Document): "make_serial_no_batch_from_work_order")): return if self.has_batch_no: - self.set("batches", []) self.create_batch_for_finished_good() - args = {"item_code": self.production_item} + args = { + "item_code": self.production_item, + "work_order": self.name + } if self.has_serial_no: self.make_serial_nos(args) @@ -305,29 +307,29 @@ class WorkOrder(Document): qty = total_qty total_qty = 0 - batch = make_batch(self.production_item) - self.append("batches", { - "batch_no": batch, - "qty": qty, - }) + make_batch(frappe._dict({ + "item": self.production_item, + "qty_to_produce": qty, + "reference_doctype": self.doctype, + "reference_name": self.name + })) def delete_auto_created_batch_and_serial_no(self): - if self.serial_no: - for d in get_serial_nos(self.serial_no): - frappe.delete_doc("Serial No", d) + for row in frappe.get_all("Serial No", filters = {"work_order": self.name}): + frappe.delete_doc("Serial No", row.name) + self.db_set("serial_no", "") - for row in self.batches: - batch_no = row.batch_no - row.db_set("batch_no", None) - frappe.delete_doc("Batch", batch_no) + for row in frappe.get_all("Batch", filters = {"reference_name": self.name}): + frappe.delete_doc("Batch", row.name) def make_serial_nos(self, args): serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") if serial_no_series: self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) - elif self.serial_no: - args.update({"serial_no": self.serial_no, "actual_qty": self.qty, "batch_no": self.batch_no}) - self.serial_no = auto_make_serial_nos(args) + + if self.serial_no: + args.update({"serial_no": self.serial_no, "actual_qty": self.qty}) + auto_make_serial_nos(args) serial_nos_length = len(get_serial_nos(self.serial_no)) if serial_nos_length != self.qty: @@ -341,6 +343,7 @@ class WorkOrder(Document): plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 for index, row in enumerate(self.operations): + if row.skip_job_card: continue qty = self.qty i=0 while qty > 0: @@ -493,7 +496,7 @@ class WorkOrder(Document): select operation, description, workstation, idx, base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id + "Pending" as status, parent as bom, batch_size, sequence_id, skip_job_card from `tabBOM Operation` where @@ -755,14 +758,16 @@ class WorkOrder(Document): bom.set_bom_material_details() return bom - def update_batch_qty(self): - if self.has_batch_no and self.batches: - for row in self.batches: - qty = frappe.get_all("Stock Entry Detail", fields = ["sum(transfer_qty)"], - filters = {"docstatus": 1, "batch_no": row.batch_no, "is_finished_item": 1}, as_list=1) + def update_batch_produced_qty(self, stock_entry_doc): + if not cint(frappe.db.get_single_value("Manufacturing Settings", + "make_serial_no_batch_from_work_order")): return - if qty: - frappe.db.set_value("Work Order Batch", row.name, "produced_qty", flt(qty[0][0])) + for row in stock_entry_doc.items: + if row.batch_no and (row.is_finished_item or row.is_scrap_item): + qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no}, + or_conditions= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"])[0][0] + + frappe.db.set_value("Batch", row.batch_no, "produced_qty", qty) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 87c090f99c..9aa0715e7f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -4,10 +4,17 @@ from frappe import _ def get_data(): return { 'fieldname': 'work_order', + 'non_standard_fieldnames': { + 'Batch': 'reference_name' + }, 'transactions': [ { 'label': _('Transactions'), 'items': ['Stock Entry', 'Job Card', 'Pick List'] + }, + { + 'label': _('Reference'), + 'items': ['Serial No', 'Batch'] } ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_batch/__init__.py b/erpnext/manufacturing/doctype/work_order_batch/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json deleted file mode 100644 index ad667b7c39..0000000000 --- a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "actions": [], - "creation": "2021-01-04 16:42:39.347528", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "batch_no", - "qty", - "produced_qty" - ], - "fields": [ - { - "fieldname": "batch_no", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Batch No", - "options": "Batch" - }, - { - "fieldname": "qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Qty", - "non_negative": 1 - }, - { - "default": "0", - "fieldname": "produced_qty", - "fieldtype": "Float", - "label": "Produced Qty", - "no_copy": 1, - "print_hide": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-01-05 10:57:07.278399", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Work Order Batch", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py b/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py deleted file mode 100644 index cf3ec475ca..0000000000 --- a/erpnext/manufacturing/doctype/work_order_batch/work_order_batch.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -# import frappe -from frappe.model.document import Document - -class WorkOrderBatch(Document): - pass diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 8c5cde9a13..b77690997c 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -8,8 +8,10 @@ "details", "operation", "bom", - "sequence_id", + "column_break_4", + "skip_job_card", "description", + "sequence_id", "col_break1", "completed_qty", "status", @@ -195,12 +197,23 @@ "label": "Sequence ID", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "skip_job_card", + "fieldtype": "Check", + "label": "Skip Job Card" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-14 12:58:49.241252", + "modified": "2021-01-08 17:42:05.372163", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index 943cb3401f..e6d2e1330b 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "field:batch_id", "creation": "2013-03-05 14:50:38", @@ -25,7 +26,11 @@ "reference_doctype", "reference_name", "section_break_7", - "description" + "description", + "manufacturing_section", + "qty_to_produce", + "column_break_23", + "produced_qty" ], "fields": [ { @@ -160,13 +165,35 @@ "label": "Batch UOM", "options": "UOM", "read_only": 1 + }, + { + "fieldname": "manufacturing_section", + "fieldtype": "Section Break", + "label": "Manufacturing" + }, + { + "fieldname": "qty_to_produce", + "fieldtype": "Float", + "label": "Qty To Produce", + "read_only": 1 + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "fieldname": "produced_qty", + "fieldtype": "Float", + "label": "Produced Qty", + "read_only": 1 } ], "icon": "fa fa-archive", "idx": 1, "image_field": "image", + "links": [], "max_attachments": 5, - "modified": "2020-09-18 17:26:09.703215", + "modified": "2021-01-07 11:10:09.149170", "modified_by": "Administrator", "module": "Stock", "name": "Batch", diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 07cf08a5bb..bb5ad5c6fe 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -310,9 +310,7 @@ def validate_serial_no_with_batch(serial_nos, item_code): frappe.throw(_("There is no batch found against the {0}: {1}") .format(message, serial_no_link)) -def make_batch(item_code): - if frappe.db.get_value("Item", item_code, "has_batch_no"): - doc = frappe.new_doc("Batch") - doc.item = item_code - doc.save() - return doc.name \ No newline at end of file +def make_batch(args): + if frappe.db.get_value("Item", args.item, "has_batch_no"): + args.doctype = "Batch" + frappe.get_doc(args).insert().name \ No newline at end of file diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 3acf3a9316..a3d44af494 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -57,7 +57,8 @@ "more_info", "serial_no_details", "company", - "status" + "status", + "work_order" ], "fields": [ { @@ -422,12 +423,18 @@ "label": "Status", "options": "\nActive\nInactive\nDelivered\nExpired", "read_only": 1 + }, + { + "fieldname": "work_order", + "fieldtype": "Link", + "label": "Work Order", + "options": "Work Order" } ], "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2020-07-20 20:50:16.660433", + "modified": "2021-01-08 14:31:15.375996", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index b236f6a999..bad7b608ac 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -473,16 +473,13 @@ def get_serial_nos(serial_no): if s.strip()] def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): - serial_no_doc.update({ - "item_code": args.get("item_code"), - "company": args.get("company"), - "batch_no": args.get("batch_no"), - "via_stock_ledger": args.get("via_stock_ledger") or True, - "supplier": args.get("supplier"), - "location": args.get("location"), - "warehouse": (args.get("warehouse") - if args.get("actual_qty", 0) > 0 else None) - }) + for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: + if args.get(field): + serial_no_doc.set(field, args.get(field)) + + serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True + serial_no_doc.warehouse = (args.get("warehouse") + if args.get("actual_qty", 0) > 0 else None) if is_new: serial_no_doc.serial_no = serial_no diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5fde35a811..83412c61d9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -855,7 +855,7 @@ class StockEntry(StockController): pro_doc.run_method("update_work_order_qty") if self.purpose == "Manufacture": pro_doc.run_method("update_planned_qty") - pro_doc.update_batch_qty() + pro_doc.update_batch_produced_qty(self) if not pro_doc.operations: pro_doc.set_actual_dates() @@ -1090,14 +1090,21 @@ class StockEntry(StockController): "is_finished_item": 1 } - if self.work_order and self.pro_doc.batches: + if self.work_order and self.pro_doc.has_batch_no: self.set_batchwise_finished_goods(args, item) else: self.add_finisged_goods(args, item) def set_batchwise_finished_goods(self, args, item): qty = flt(self.fg_completed_qty) - for row in self.pro_doc.batches: + filters = {"reference_name": self.pro_doc.name, + "reference_doctype": self.pro_doc.doctype, + "qty_to_produce": (">", 0) + } + + fields = ["qty_to_produce as qty", "produced_qty", "name"] + + for row in frappe.get_all("Batch", filters = filters, fields = fields): batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue @@ -1110,7 +1117,7 @@ class StockEntry(StockController): qty -= batch_qty args["qty"] = fg_qty - args["batch_no"] = row.batch_no + args["batch_no"] = row.name self.add_finisged_goods(args, item) @@ -1555,7 +1562,7 @@ class StockEntry(StockController): def set_serial_no_batch_for_finished_good(self): args = {} - if self.pro_doc.serial_no or self.pro_doc.batch_no: + if self.pro_doc.serial_no: self.get_serial_nos_for_fg(args) for row in self.items: From 6a9798f305d93a879be5264e6388b36b04b7ec43 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 24 Jun 2021 18:11:33 +0530 Subject: [PATCH 227/550] fix: update leave allocation after submit (#26191) * fix: update leave allocation after submit v13 * fix: test * fix: test --- .../leave_allocation/leave_allocation.json | 5 +- .../leave_allocation/leave_allocation.py | 40 +++++++++++++++- .../leave_allocation/test_leave_allocation.py | 46 +++++++++++++++++++ .../employee_leave_balance.py | 2 +- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index ae02c512c2..3a6539ece9 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -110,6 +110,7 @@ "label": "Allocation" }, { + "allow_on_submit": 1, "bold": 1, "fieldname": "new_leaves_allocated", "fieldtype": "Float", @@ -235,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-14 15:28:26.335104", + "modified": "2021-06-03 15:28:26.335104", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -277,4 +278,4 @@ "sort_field": "modified", "sort_order": "DESC", "timeline_field": "employee" -} \ No newline at end of file +} diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 11302cad75..4757cd3b19 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry +from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period class OverlapError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass @@ -55,6 +56,43 @@ class LeaveAllocation(Document): if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) + def on_update_after_submit(self): + if self.has_value_changed("new_leaves_allocated"): + self.validate_against_leave_applications() + leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count() + args = { + "leaves": leaves_to_be_added, + "from_date": self.from_date, + "to_date": self.to_date, + "is_carry_forward": 0 + } + create_leave_ledger_entry(self, args, True) + + def get_existing_leave_count(self): + ledger_entries = frappe.get_all("Leave Ledger Entry", + filters={ + "transaction_type": "Leave Allocation", + "transaction_name": self.name, + "employee": self.employee, + "company": self.company, + "leave_type": self.leave_type + }, + pluck="leaves") + total_existing_leaves = 0 + for entry in ledger_entries: + total_existing_leaves += entry + + return total_existing_leaves + + def validate_against_leave_applications(self): + leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, + self.from_date, self.to_date) + if flt(leaves_taken) > flt(self.total_leaves_allocated): + if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): + frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) + else: + frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) + def update_leave_policy_assignments_when_no_allocations_left(self): allocations = frappe.db.get_list("Leave Allocation", filters = { "docstatus": 1, @@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date): def validate_carry_forward(leave_type): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): - frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) \ No newline at end of file + frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 6e7ae87d08..bff06e6a91 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import frappe +import erpnext import unittest from frappe.utils import nowdate, add_months, getdate, add_days from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type @@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.cancel() self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) + def test_leave_addition_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 40 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 40) + + def test_leave_subtraction_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 10 + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 10) + + def test_against_leave_application_validation_after_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + self.assertTrue(leave_allocation.total_leaves_allocated, 15) + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + leave_application = frappe.get_doc({ + "doctype": 'Leave Application', + "employee": employee.name, + "leave_type": "_Test Leave Type", + "from_date": add_months(nowdate(), 2), + "to_date": add_months(add_days(nowdate(), 10), 2), + "company": erpnext.get_default_company() or "_Test Company", + "docstatus": 1, + "status": "Approved", + "leave_approver": 'test@example.com' + }) + leave_application.submit() + leave_allocation.new_leaves_allocated = 8 + leave_allocation.total_leaves_allocated = 8 + self.assertRaises(frappe.ValidationError, leave_allocation.submit) + def create_leave_allocation(**args): args = frappe._dict(args) diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 4dd4570e8c..b8953b3eaa 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 AND leaves>0 + AND docstatus=1 AND (from_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s OR (from_date < %(from_date)s AND to_date > %(to_date)s)) From c878389050a45fa6cdebf057da1752242db0ad3f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 8 Jan 2021 19:47:38 +0530 Subject: [PATCH 228/550] fix: or condition filter in the get_all --- .../doctype/job_card/job_card.json | 8 +- .../doctype/job_card/job_card.py | 20 +-- .../doctype/job_card_item/job_card_item.json | 13 ++ .../doctype/work_order/work_order.py | 9 +- .../cost_of_poor_quality_report/__init__.py | 0 .../cost_of_poor_quality_report.js | 9 ++ .../cost_of_poor_quality_report.json | 33 +++++ .../cost_of_poor_quality_report.py | 136 ++++++++++++++++++ erpnext/patches.txt | 3 +- .../patches/v13_0/update_job_card_details.py | 16 +++ .../stock/doctype/stock_entry/stock_entry.py | 3 +- 11 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json create mode 100644 erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py create mode 100644 erpnext/patches/v13_0/update_job_card_details.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index c2fd8cc3f9..0597cdb207 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -33,6 +33,7 @@ "total_completed_qty", "column_break_15", "total_time_in_mins", + "hour_rate", "section_break_8", "items", "more_information", @@ -328,11 +329,16 @@ "fieldname": "section_break_21", "fieldtype": "Section Break", "hide_border": 1 + }, + { + "fieldname": "hour_rate", + "fieldtype": "Currency", + "label": "Hour Rate" } ], "is_submittable": 1, "links": [], - "modified": "2020-12-14 15:14:05.566271", + "modified": "2021-01-11 12:09:00.452032", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 5c157d43ec..b2d5667368 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -41,6 +41,7 @@ class JobCard(Document): def validate_time_logs(self): self.total_time_in_mins = 0.0 + self.total_completed_qty = 0.0 if self.get('time_logs'): for d in self.get('time_logs'): @@ -58,8 +59,6 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty - else: - self.total_completed_qty = 0.0 self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) @@ -256,12 +255,14 @@ class JobCard(Document): if self.get('operation') == d.operation: self.append('items', { - 'item_code': d.item_code, - 'source_warehouse': d.source_warehouse, - 'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'), - 'item_name': d.item_name, - 'description': d.description, - 'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty + "item_code": d.item_code, + "source_warehouse": d.source_warehouse, + "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'), + "item_name": d.item_name, + "description": d.description, + "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, + "rate": d.rate, + "amount": d.amount }) def on_submit(self): @@ -439,7 +440,8 @@ class JobCard(Document): data = frappe.get_all("Work Order Operation", fields = ["operation", "status", "completed_qty"], - filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)}, + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id), + "skip_job_card": 0}, order_by = "sequence_id, idx") message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name), diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 100ef4ca3a..60a2249442 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -17,6 +17,8 @@ "required_qty", "column_break_9", "transferred_qty", + "rate", + "amount", "allow_alternative_item" ], "fields": [ @@ -101,6 +103,17 @@ "label": "Transferred Qty", "no_copy": 1, "print_hide": 1, + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", "read_only": 1 } ], diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 23cc090427..06cafd2d04 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -764,10 +764,10 @@ class WorkOrder(Document): for row in stock_entry_doc.items: if row.batch_no and (row.is_finished_item or row.is_scrap_item): - qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no}, - or_conditions= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"])[0][0] + qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1}, + or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0] - frappe.db.set_value("Batch", row.batch_no, "produced_qty", qty) + frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty)) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs @@ -1006,7 +1006,8 @@ def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto 'project': work_order.project, 'company': work_order.company, 'sequence_id': row.get("sequence_id"), - 'wip_warehouse': work_order.wip_warehouse + 'wip_warehouse': work_order.wip_warehouse, + "hour_rate": row.get("hour_rate") }) if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js new file mode 100644 index 0000000000..7f5bc48f18 --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -0,0 +1,9 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Cost of Poor Quality Report"] = { + "filters": [ + + ] +}; diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json new file mode 100644 index 0000000000..ee63bc1c28 --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-01-11 11:10:58.292896", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2021-01-11 11:11:03.594242", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Cost of Poor Quality Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Job Card", + "report_name": "Cost of Poor Quality Report", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py new file mode 100644 index 0000000000..21e7be7478 --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -0,0 +1,136 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt + +def execute(filters=None): + columns, data = [], [] + + columns = get_columns(filters) + data = get_data(filters) + + return columns, data + +def get_data(filters): + data = [] + operations = frappe.get_all("Operation", filters = {"cost_of_poor_quality_operation": 1}) + if operations: + operations = [d.name for d in operations] + fields = ["production_item as item_code", "item_name", "work_order", "operation", + "workstation", "total_time_in_mins", "name", "hour_rate"] + + job_cards = frappe.get_all("Job Card", fields = fields, + filters = {"docstatus": 1, "operation": ("in", operations)}) + + for row in job_cards: + row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) + update_raw_material_cost(row, filters) + update_time_details(row, filters, data) + + return data + +def update_raw_material_cost(row, filters): + row.rm_cost = 0.0 + for data in frappe.get_all("Job Card Item", fields = ["amount"], + filters={"parent": row.name, "docstatus": 1}): + row.rm_cost += data.amount + +def update_time_details(row, filters, data): + args = frappe._dict({"item_code": "", "item_name": "", "name": "", "work_order":"", + "operation": "", "workstation":"", "operating_cost": "", "rm_cost": "", "total_time_in_mins": ""}) + + i=0 + for time_log in frappe.get_all("Job Card Time Log", fields = ["from_time", "to_time", "time_in_mins"], + filters={"parent": row.name, "docstatus": 1}): + + if i==0: + i += 1 + row.update(time_log) + data.append(row) + else: + args.update(time_log) + data.append(args) + +def get_columns(filters): + return [ + { + "label": _("Job Card"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Job Card", + "width": "100" + }, + { + "label": _("Work Order"), + "fieldtype": "Link", + "fieldname": "work_order", + "options": "Work Order", + "width": "100" + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": "100" + }, + { + "label": _("Item Name"), + "fieldtype": "Data", + "fieldname": "item_name", + "width": "100" + }, + { + "label": _("Operation"), + "fieldtype": "Link", + "fieldname": "operation", + "options": "Operation", + "width": "100" + }, + { + "label": _("Workstation"), + "fieldtype": "Link", + "fieldname": "workstation", + "options": "Workstation", + "width": "100" + }, + { + "label": _("Operating Cost"), + "fieldtype": "Currency", + "fieldname": "operating_cost", + "width": "100" + }, + { + "label": _("Raw Material Cost"), + "fieldtype": "Currency", + "fieldname": "rm_cost", + "width": "100" + }, + { + "label": _("Total Time (in Mins)"), + "fieldtype": "Float", + "fieldname": "total_time_in_mins", + "width": "100" + }, + { + "label": _("From Time"), + "fieldtype": "Datetime", + "fieldname": "from_time", + "width": "100" + }, + { + "label": _("To Time"), + "fieldtype": "Datetime", + "fieldname": "to_time", + "width": "100" + }, + { + "label": _("Time in Mins"), + "fieldtype": "Float", + "fieldname": "time_in_mins", + "width": "100" + }, + ] \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index dd0e33beba..2b1fc43a1c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -288,4 +288,5 @@ execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold -erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice \ No newline at end of file +erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice +erpnext.patches.v13_0.update_job_card_details diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py new file mode 100644 index 0000000000..d4e65c6f2f --- /dev/null +++ b/erpnext/patches/v13_0/update_job_card_details.py @@ -0,0 +1,16 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "job_card") + frappe.reload_doc("manufacturing", "doctype", "job_card_item") + frappe.reload_doc("manufacturing", "doctype", "work_order_operation") + + frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo + SET jc.hour_rate = wo.hour_rate + WHERE + jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0 + """) \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 83412c61d9..4cc721badf 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -365,6 +365,7 @@ class StockEntry(StockController): "overproduction_percentage_for_work_order")) for d in prod_order.get("operations"): + if d.skip_job_card: continue total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty) if total_completed_qty > flt(completed_qty): @@ -1104,7 +1105,7 @@ class StockEntry(StockController): fields = ["qty_to_produce as qty", "produced_qty", "name"] - for row in frappe.get_all("Batch", filters = filters, fields = fields): + for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue From 57307443f04c7645889e9e8f41670f18f9ba63ee Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Jan 2021 18:32:33 +0530 Subject: [PATCH 229/550] is corrective job card --- .../doctype/bom_operation/bom_operation.json | 10 +-- .../doctype/job_card/job_card.js | 53 +++++++++++-- .../doctype/job_card/job_card.json | 43 +++++++++- .../doctype/job_card/job_card.py | 78 +++++++++++++++---- .../doctype/job_card_item/job_card_item.json | 2 +- .../doctype/operation/operation.json | 17 ++-- .../doctype/work_order/work_order.js | 4 +- .../doctype/work_order/work_order.json | 11 +++ .../doctype/work_order/work_order.py | 8 +- .../work_order_operation.json | 10 +-- .../cost_of_poor_quality_report.js | 62 ++++++++++++++- .../cost_of_poor_quality_report.py | 26 +++++-- .../stock/doctype/stock_entry/stock_entry.py | 1 - 13 files changed, 260 insertions(+), 65 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 1330636198..4458e6db23 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -11,7 +11,6 @@ "workstation", "description", "col_break1", - "skip_job_card", "hour_rate", "time_in_mins", "operating_cost", @@ -118,20 +117,13 @@ "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID" - }, - { - "allow_on_submit": 1, - "default": "0", - "fieldname": "skip_job_card", - "fieldtype": "Check", - "label": "Skip Job Card" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-05 14:29:11.887888", + "modified": "2021-01-12 14:48:09.596843", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 57ec20b42c..266d5f6058 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -41,6 +41,10 @@ frappe.ui.form.on('Job Card', { } } + if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { + frm.trigger('setup_corrective_job_card') + } + frm.set_query("quality_inspection", function() { return { query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", @@ -53,12 +57,50 @@ frappe.ui.form.on('Job Card', { frm.trigger("toggle_operation_number"); - if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) + if (frm.doc.docstatus == 0 && !frm.is_new() && + (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, + setup_corrective_job_card: function(frm) { + frm.add_custom_button(__('Corrective Job Card'), () => { + let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation); + + let fields = [ + { + fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', + fieldname: 'operation', get_query() { return { filters: { "is_corrective_operation": 1 }}} + }, { + fieldtype: 'Link', label: __('For Operation'), options: 'Operation', + fieldname: 'for_operation', get_query() { return { filters: { "name": ["in", operations] }}} + } + ]; + + frappe.prompt(fields, d => { + frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); + }, __("Select Corrective Operation")); + }, __('Make')); + }, + + make_corrective_job_card: function(frm, operation, for_operation) { + frappe.call({ + method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card', + args: { + source_name: frm.doc.name, + operation: operation, + for_operation: for_operation + }, + callback: function(r) { + if (r.message) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }); + }, + operation: function(frm) { frm.trigger("toggle_operation_number"); @@ -110,10 +152,9 @@ frappe.ui.form.on('Job Card', { if (!frm.doc.started_time && !frm.doc.current_time) { frm.add_custom_button(__("Start Job"), () => { - frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Employee'), options: "Job Card Time Log", - fieldname: 'employee'}, d => { - debugger - frm.events.start_job(frm, "Work In Progress", d.employee); + frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), + options: "Job Card Time Log", fieldname: 'employees'}, d => { + frm.events.start_job(frm, "Work In Progress", d.employees); }, __("Assign Job to Employee")); }).addClass("btn-primary"); } else if (frm.doc.status == "On Hold") { @@ -138,7 +179,7 @@ frappe.ui.form.on('Job Card', { const args = { job_card_id: frm.doc.name, start_time: frappe.datetime.now_datetime(), - employee: employee, + employees: employee, status: status }; frm.events.make_time_log(frm, args); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 0597cdb207..be7a810173 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -33,9 +33,14 @@ "total_completed_qty", "column_break_15", "total_time_in_mins", - "hour_rate", "section_break_8", "items", + "corrective_operation_section", + "for_job_card", + "is_corrective_job_card", + "column_break_33", + "hour_rate", + "for_operation", "more_information", "operation_id", "sequence_id", @@ -331,14 +336,48 @@ "hide_border": 1 }, { + "depends_on": "is_corrective_job_card", "fieldname": "hour_rate", "fieldtype": "Currency", "label": "Hour Rate" + }, + { + "collapsible": 1, + "depends_on": "is_corrective_job_card", + "fieldname": "corrective_operation_section", + "fieldtype": "Section Break", + "label": "Corrective Operation" + }, + { + "default": "0", + "fieldname": "is_corrective_job_card", + "fieldtype": "Check", + "label": "Is Corrective Job Card", + "read_only": 1 + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" + }, + { + "fieldname": "for_job_card", + "fieldtype": "Link", + "label": "For Job Card", + "options": "Job Card", + "read_only": 1 + }, + { + "fetch_from": "for_job_card.operation", + "fetch_if_empty": 1, + "fieldname": "for_operation", + "fieldtype": "Link", + "label": "For Operation", + "options": "Operation" } ], "is_submittable": 1, "links": [], - "modified": "2021-01-11 12:09:00.452032", + "modified": "2021-02-03 20:36:51.826944", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index b2d5667368..b4202e158d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -178,18 +178,27 @@ class JobCard(Document): self.reset_timer_value(args) if last_row and args.get("complete_time"): - last_row.update({ - "to_time": get_datetime(args.get("complete_time")), - "operation": args.get("sub_operation"), - "completed_qty": args.get("completed_qty") or 0.0 - }) + for row in self.time_logs: + if not row.to_time: + row.update({ + "to_time": get_datetime(args.get("complete_time")), + "operation": args.get("sub_operation"), + "completed_qty": args.get("completed_qty") or 0.0 + }) elif args.get("start_time"): - self.append("time_logs", { - "from_time": get_datetime(args.get("start_time")), - "employee": args.get("employee"), - "operation": args.get("sub_operation"), - "completed_qty": 0.0 - }) + employees = args.employees + print(args) + if isinstance(employees, string_types): + employees = json.loads(employees) + + for name in employees: + print(name.get('employee')) + self.append("time_logs", { + "from_time": get_datetime(args.get("start_time")), + "employee": name.get('employee'), + "operation": args.get("sub_operation"), + "completed_qty": 0.0 + }) if self.status == "On Hold": self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) @@ -300,10 +309,24 @@ class JobCard(Document): time_in_mins = flt(data[0].time_in_mins) wo = frappe.get_doc('Work Order', self.work_order) - if self.operation_id: + + if self.is_corrective_job_card: + self.update_corrective_in_work_order(wo) + + elif self.operation_id: self.validate_produced_quantity(for_quantity, wo) self.update_work_order_data(for_quantity, time_in_mins, wo) + def update_corrective_in_work_order(self, wo): + wo.corrective_operation_cost = 0.0 + for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'], + filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}): + wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) + + wo.calculate_operating_cost() + wo.flags.ignore_validate_update_after_submit = True + wo.save() + def validate_produced_quantity(self, for_quantity, wo): if self.docstatus < 2: return @@ -346,7 +369,8 @@ class JobCard(Document): def get_current_operation_data(self): return frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) + filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id, + "is_corrective_job_card": 0}) def set_transferred_qty_in_job_card(self, ste_doc): for row in ste_doc.items: @@ -429,6 +453,8 @@ class JobCard(Document): .format(bold(self.operation), work_order), OperationMismatchError) def validate_sequence_id(self): + if self.is_corrective_job_card: return + if not (self.work_order and self.sequence_id): return current_operation_qty = 0.0 @@ -440,8 +466,7 @@ class JobCard(Document): data = frappe.get_all("Work Order Operation", fields = ["operation", "status", "completed_qty"], - filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id), - "skip_job_card": 0}, + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)}, order_by = "sequence_id, idx") message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name), @@ -598,3 +623,26 @@ def get_job_details(start, end, filters=None): events.append(job_card_data) return events + +@frappe.whitelist() +def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None): + def set_missing_values(source, target): + target.is_corrective_job_card = 1 + target.operation = operation + target.for_operation = for_operation + + target.set('time_logs', []) + target.get_sub_operations() + target.get_required_items() + target.validate_time_logs() + + doclist = get_mapped_doc("Job Card", source_name, { + "Job Card": { + "doctype": "Job Card", + "field_map": { + "name": "for_job_card", + }, + } + }, target_doc, set_missing_values) + + return doclist \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index 60a2249442..a239a247e3 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -102,7 +102,7 @@ "fieldtype": "Float", "label": "Transferred Qty", "no_copy": 1, - "print_hide": 1, + "print_hide": 1 }, { "fieldname": "rate", diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json index 9e6f8e1f5d..10a97eda76 100644 --- a/erpnext/manufacturing/doctype/operation/operation.json +++ b/erpnext/manufacturing/doctype/operation/operation.json @@ -10,7 +10,7 @@ "field_order": [ "workstation", "data_2", - "cost_of_poor_quality_operation", + "is_corrective_operation", "job_card_section", "create_job_card_based_on_batch_size", "column_break_6", @@ -77,13 +77,6 @@ "fieldtype": "Check", "label": "Create Job Card based on Batch Size" }, - { - "default": "0", - "description": "Cost of poor quality operation", - "fieldname": "cost_of_poor_quality_operation", - "fieldtype": "Check", - "label": "Is COPQ Operation" - }, { "collapsible": 1, "fieldname": "job_card_section", @@ -93,12 +86,18 @@ { "fieldname": "column_break_6", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_corrective_operation", + "fieldtype": "Check", + "label": "Is Corrective Operation" } ], "icon": "fa fa-wrench", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-24 14:25:03.428303", + "modified": "2021-01-12 15:09:23.593338", "modified_by": "Administrator", "module": "Manufacturing", "name": "Operation", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index adf6453e2e..acb3407e2b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -242,13 +242,13 @@ frappe.ui.form.on("Work Order", { if(data.completed_qty != frm.doc.qty) { pending_qty = frm.doc.qty - flt(data.completed_qty); - if (pending_qty && !data.skip_job_card) { + if (pending_qty) { dialog.fields_dict.operations.df.data.push({ 'name': data.name, 'operation': data.operation, 'workstation': data.workstation, 'qty': pending_qty, - 'pending_qty': pending_qty, + 'pending_qty': pending_qty }); } } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index c80decb92e..8e99c665f1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -58,6 +58,7 @@ "actual_operating_cost", "additional_operating_cost", "column_break_24", + "corrective_operation_cost", "total_operating_cost", "more_info", "description", @@ -534,6 +535,16 @@ "fieldname": "batch_size", "fieldtype": "Float", "label": "Batch Size" + }, + { + "allow_on_submit": 1, + "description": "From Corrective Job Card", + "fieldname": "corrective_operation_cost", + "fieldtype": "Currency", + "label": "Corrective Operation Cost", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-cogs", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 06cafd2d04..c83f539e03 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -130,7 +130,9 @@ class WorkOrder(Document): variable_cost = self.actual_operating_cost if self.actual_operating_cost \ else self.planned_operating_cost - self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost) + + self.total_operating_cost = (flt(self.additional_operating_cost) + + flt(variable_cost) + flt(self.corrective_operation_cost)) def validate_work_order_against_so(self): # already ordered qty @@ -343,7 +345,6 @@ class WorkOrder(Document): plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 for index, row in enumerate(self.operations): - if row.skip_job_card: continue qty = self.qty i=0 while qty > 0: @@ -357,6 +358,7 @@ class WorkOrder(Document): qty -= row.batch_size elif qty > 0: job_card_qty = qty + qty = 0 if job_card_qty > 0: self.prepare_data_for_job_card(row, job_card_qty, index, @@ -496,7 +498,7 @@ class WorkOrder(Document): select operation, description, workstation, idx, base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id, skip_job_card + "Pending" as status, parent as bom, batch_size, sequence_id from `tabBOM Operation` where diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index b77690997c..6d8fb80e31 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -9,7 +9,6 @@ "operation", "bom", "column_break_4", - "skip_job_card", "description", "sequence_id", "col_break1", @@ -201,19 +200,12 @@ { "fieldname": "column_break_4", "fieldtype": "Column Break" - }, - { - "allow_on_submit": 1, - "default": "0", - "fieldname": "skip_job_card", - "fieldtype": "Check", - "label": "Skip Job Card" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-08 17:42:05.372163", + "modified": "2021-01-12 14:48:31.061286", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js index 7f5bc48f18..ef77566389 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -4,6 +4,66 @@ frappe.query_reports["Cost of Poor Quality Report"] = { "filters": [ - + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + label: __("From Date"), + fieldname:"from_date", + fieldtype: "Datetime", + default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), + reqd: 1 + }, + { + label: __("To Date"), + fieldname:"to_date", + fieldtype: "Datetime", + default: frappe.datetime.now_datetime(), + reqd: 1, + }, + { + label: __("Job Card"), + fieldname: "name", + fieldtype: "Link", + options: "Job Card", + get_query: function() { + return { + filters: { + is_corrective_job_card: 1, + docstatus: 1 + } + } + } + }, + { + label: __("Work Order"), + fieldname: "work_order", + fieldtype: "Link", + options: "Work Order" + }, + { + label: __("Operation"), + fieldname: "operation", + fieldtype: "Link", + options: "Operation", + get_query: function() { + return { + filters: { + is_corrective_operation: 1 + } + } + } + }, + { + label: __("Workstation"), + fieldname: "workstation", + fieldtype: "Link", + options: "Workstation" + }, ] }; diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 21e7be7478..2e8c191c60 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -14,24 +14,34 @@ def execute(filters=None): return columns, data -def get_data(filters): +def get_data(report_filters): data = [] - operations = frappe.get_all("Operation", filters = {"cost_of_poor_quality_operation": 1}) + operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1}) if operations: operations = [d.name for d in operations] fields = ["production_item as item_code", "item_name", "work_order", "operation", "workstation", "total_time_in_mins", "name", "hour_rate"] + filters = get_filters(report_filters, operations) + job_cards = frappe.get_all("Job Card", fields = fields, - filters = {"docstatus": 1, "operation": ("in", operations)}) + filters = filters) for row in job_cards: row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) - update_raw_material_cost(row, filters) - update_time_details(row, filters, data) + update_raw_material_cost(row, report_filters) + update_time_details(row, report_filters, data) return data +def get_filters(report_filters, operations): + filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} + for field in ["name", "work_order", "operation", "workstation", "company"]: + if report_filters.get(field): + filters[field] = report_filters.get(field) + + return filters + def update_raw_material_cost(row, filters): row.rm_cost = 0.0 for data in frappe.get_all("Job Card Item", fields = ["amount"], @@ -43,8 +53,10 @@ def update_time_details(row, filters, data): "operation": "", "workstation":"", "operating_cost": "", "rm_cost": "", "total_time_in_mins": ""}) i=0 - for time_log in frappe.get_all("Job Card Time Log", fields = ["from_time", "to_time", "time_in_mins"], - filters={"parent": row.name, "docstatus": 1}): + for time_log in frappe.get_all("Job Card Time Log", + fields = ["from_time", "to_time", "time_in_mins"], + filters={"parent": row.name, "docstatus": 1, + "from_time": (">=", filters.from_date), "to_time": ("<=", filters.to_date)}): if i==0: i += 1 diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4cc721badf..e49c9a57c3 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -365,7 +365,6 @@ class StockEntry(StockController): "overproduction_percentage_for_work_order")) for d in prod_order.get("operations"): - if d.skip_job_card: continue total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty) if total_completed_qty > flt(completed_qty): From 2330c41ccae1777c063a14024c4cdd42aeb9c921 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 17 Mar 2021 14:03:12 +0530 Subject: [PATCH 230/550] fix: total time calculation --- erpnext/manufacturing/doctype/bom/bom.js | 8 -- erpnext/manufacturing/doctype/bom/bom.json | 5 +- erpnext/manufacturing/doctype/bom/bom.py | 2 +- .../doctype/job_card/job_card.js | 56 ++++++++--- .../doctype/job_card/job_card.json | 25 ++++- .../doctype/job_card/job_card.py | 89 ++++++++++++----- .../doctype/job_card_item/job_card_item.json | 23 +---- .../job_card_operation.json | 11 ++- .../manufacturing_settings.json | 9 +- .../doctype/operation/operation.js | 10 +- .../doctype/operation/operation.py | 1 - .../doctype/work_order/work_order.js | 43 +++++--- .../doctype/work_order/work_order.json | 5 +- .../doctype/work_order/work_order.py | 97 ++++++++++++------- .../cost_of_poor_quality_report.js | 36 +++++++ .../cost_of_poor_quality_report.py | 61 ++++-------- .../stock/doctype/stock_entry/stock_entry.py | 10 +- .../stock_entry_detail.json | 4 +- 18 files changed, 324 insertions(+), 171 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index a09a5e3430..27019dbbae 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", { refresh: function(frm) { frm.toggle_enable("item", frm.doc.__islocal); - toggle_operations(frm); frm.set_indicator_formatter('item_code', function(doc) { @@ -651,15 +650,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) { erpnext.bom.calculate_total(frm.doc); }); -var toggle_operations = function(frm) { - frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1); - frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1); - frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1); -}; - frappe.ui.form.on("BOM", "with_operations", function(frm) { if(!cint(frm.doc.with_operations)) { frm.set_value("operations", []); } - toggle_operations(frm); }); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index f551b91597..f38d1b9892 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -193,6 +193,7 @@ }, { "default": "Work Order", + "depends_on": "with_operations", "fieldname": "transfer_material_against", "fieldtype": "Select", "label": "Transfer Material Against", @@ -235,6 +236,7 @@ { "fieldname": "operations_section", "fieldtype": "Section Break", + "hide_border": 1, "oldfieldtype": "Section Break" }, { @@ -245,6 +247,7 @@ "options": "Routing" }, { + "depends_on": "with_operations", "fieldname": "operations", "fieldtype": "Table", "label": "Operations", @@ -517,7 +520,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-05-21 12:29:32.634952", + "modified": "2021-03-16 12:25:09.081968", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3f109d91b5..3e855603b4 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -590,7 +590,7 @@ class BOM(WebsiteGenerator): self.get_routing() def validate_operations(self): - if self.with_operations and not self.get('operations'): + if self.with_operations and not self.get('operations') and self.docstatus == 1: frappe.throw(_("Operations cannot be left blank")) if self.with_operations: diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 266d5f6058..81860c9fbc 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -42,7 +42,7 @@ frappe.ui.form.on('Job Card', { } if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) { - frm.trigger('setup_corrective_job_card') + frm.trigger('setup_corrective_job_card'); } frm.set_query("quality_inspection", function() { @@ -71,15 +71,27 @@ frappe.ui.form.on('Job Card', { let fields = [ { fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation', - fieldname: 'operation', get_query() { return { filters: { "is_corrective_operation": 1 }}} + fieldname: 'operation', get_query() { + return { + filters: { + "is_corrective_operation": 1 + } + }; + } }, { fieldtype: 'Link', label: __('For Operation'), options: 'Operation', - fieldname: 'for_operation', get_query() { return { filters: { "name": ["in", operations] }}} + fieldname: 'for_operation', get_query() { + return { + filters: { + "name": ["in", operations] + } + }; + } } ]; frappe.prompt(fields, d => { - frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); + frm.events.make_corrective_job_card(frm, d.operation, d.for_operation); }, __("Select Corrective Operation")); }, __('Make')); }, @@ -152,14 +164,18 @@ frappe.ui.form.on('Job Card', { if (!frm.doc.started_time && !frm.doc.current_time) { frm.add_custom_button(__("Start Job"), () => { - frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), - options: "Job Card Time Log", fieldname: 'employees'}, d => { + if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) { + frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'), + options: "Job Card Time Log", fieldname: 'employees'}, d => { frm.events.start_job(frm, "Work In Progress", d.employees); - }, __("Assign Job to Employee")); + }, __("Assign Job to Employee")); + } else { + frm.events.start_job(frm, "Work In Progress", frm.doc.employee); + } }).addClass("btn-primary"); } else if (frm.doc.status == "On Hold") { frm.add_custom_button(__("Resume Job"), () => { - frm.events.start_job(frm, "Resume Job"); + frm.events.start_job(frm, "Resume Job", frm.doc.employee); }).addClass("btn-primary"); } else { frm.add_custom_button(__("Pause Job"), () => { @@ -167,10 +183,26 @@ frappe.ui.form.on('Job Card', { }); frm.add_custom_button(__("Complete Job"), () => { - frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), - fieldname: 'qty', default: frm.doc.for_quantity}, data => { + var sub_operations = frm.doc.sub_operations; + + let set_qty = true; + if (sub_operations && sub_operations.length > 1) { + set_qty = false; + let last_op_row = sub_operations[sub_operations.length - 2]; + + if (last_op_row.status == 'Complete') { + set_qty = true; + } + } + + if (set_qty) { + frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'), + fieldname: 'qty', default: frm.doc.for_quantity}, data => { frm.events.complete_job(frm, "Complete", data.qty); }, __("Enter Value")); + } else { + frm.events.complete_job(frm, "Complete", 0.0); + } }).addClass("btn-primary"); } }, @@ -204,11 +236,11 @@ frappe.ui.form.on('Job Card', { args: args }, freeze: true, - callback: function (r) { + callback: function () { frm.reload_doc(); frm.trigger("make_dashboard"); } - }) + }); }, update_sub_operation: function(frm, args) { diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index be7a810173..046e2fd182 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -16,15 +16,18 @@ "production_item", "item_name", "for_quantity", + "serial_no", "column_break_12", "wip_warehouse", "quality_inspection", "project", + "batch_no", "operation_section_section", "operation", "operation_row_number", "column_break_18", "workstation", + "employee", "section_break_21", "sub_operations", "timing_detail", @@ -163,8 +166,7 @@ "fieldname": "items", "fieldtype": "Table", "label": "Items", - "options": "Job Card Item", - "read_only": 1 + "options": "Job Card Item" }, { "collapsible": 1, @@ -373,11 +375,28 @@ "fieldtype": "Link", "label": "For Operation", "options": "Operation" + }, + { + "fieldname": "employee", + "fieldtype": "Table MultiSelect", + "label": "Employee", + "options": "Job Card Time Log" + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" } ], "is_submittable": 1, "links": [], - "modified": "2021-02-03 20:36:51.826944", + "modified": "2021-03-16 15:59:32.766484", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index b4202e158d..7f8f2ef68d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -4,9 +4,9 @@ from __future__ import unicode_literals import frappe -import datetime, json +import datetime +import json from frappe import _, bold -from six import string_types from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, @@ -33,11 +33,10 @@ class JobCard(Document): if self.operation: self.sub_operations = [] for row in frappe.get_all("Sub Operation", - filters = {"parent": self.operation}, fields=["operation"]): - self.append("sub_operations", { - "sub_operation": row.operation, - "status": "Pending" - }) + filters = {"parent": self.operation}, fields=["operation", "idx"]): + row.status = "Pending" + row.sub_operation = row.operation + self.append("sub_operations", row) def validate_time_logs(self): self.total_time_in_mins = 0.0 @@ -57,11 +56,14 @@ class JobCard(Document): d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 self.total_time_in_mins += d.time_in_mins - if d.completed_qty: + if d.completed_qty and not self.sub_operations: self.total_completed_qty += d.completed_qty self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) + for row in self.sub_operations: + self.total_completed_qty += row.completed_qty + def get_overlap_for(self, args, check_next_available_slot=False): production_capacity = 1 @@ -173,6 +175,10 @@ class JobCard(Document): def add_time_log(self, args): last_row = [] + employees = args.employees + if isinstance(employees, str): + employees = json.loads(employees) + if self.time_logs and len(self.time_logs) > 0: last_row = self.time_logs[-1] @@ -186,13 +192,7 @@ class JobCard(Document): "completed_qty": args.get("completed_qty") or 0.0 }) elif args.get("start_time"): - employees = args.employees - print(args) - if isinstance(employees, string_types): - employees = json.loads(employees) - for name in employees: - print(name.get('employee')) self.append("time_logs", { "from_time": get_datetime(args.get("start_time")), "employee": name.get('employee'), @@ -200,11 +200,21 @@ class JobCard(Document): "completed_qty": 0.0 }) + if not self.employee: + self.set_employees(employees) + if self.status == "On Hold": self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time) self.save() + def set_employees(self, employees): + for name in employees: + self.append('employee', { + 'employee': name.get('employee'), + 'completed_qty': 0.0 + }) + def reset_timer_value(self, args): self.started_time = None @@ -221,24 +231,41 @@ class JobCard(Document): self.status = args.get("status") def update_sub_operation_status(self): - if not (self.sub_operations and self.time_logs): return + if not (self.sub_operations and self.time_logs): + return operation_wise_completed_time = {} for time_log in self.time_logs: if time_log.operation not in operation_wise_completed_time: operation_wise_completed_time.setdefault(time_log.operation, - frappe._dict({"status": "Pending", "completed_time": 0.0})) + frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []})) op_row = operation_wise_completed_time[time_log.operation] op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" + if self.status == 'On Hold': + op_row.status = 'Pause' + + op_row.employee.append(time_log.employee) if time_log.time_in_mins: op_row.completed_time += time_log.time_in_mins + op_row.completed_qty += time_log.completed_qty for row in self.sub_operations: operation_deatils = operation_wise_completed_time.get(row.sub_operation) if operation_deatils: - row.status = operation_deatils.status + if row.status != 'Complete': + row.status = operation_deatils.status + row.completed_time = operation_deatils.completed_time + if operation_deatils.employee: + row.completed_time = row.completed_time / len(set(operation_deatils.employee)) + + if operation_deatils.completed_qty: + row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee)) + else: + row.status = 'Pending' + row.completed_time = 0.0 + row.completed_qty = 0.0 def update_time_logs(self, row): self.append("time_logs", { @@ -275,6 +302,7 @@ class JobCard(Document): }) def on_submit(self): + self.validate_transfer_qty() self.validate_job_card() self.update_work_order() self.set_transferred_qty() @@ -283,7 +311,16 @@ class JobCard(Document): self.update_work_order() self.set_transferred_qty() + def validate_transfer_qty(self): + if self.items and self.transferred_qty < self.for_quantity: + frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}') + .format(self.name)) + def validate_job_card(self): + if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped': + frappe.throw(_("Transaction not allowed against stopped Work Order {0}") + .format(get_link_to_form('Work Order', self.work_order))) + if not self.time_logs: frappe.throw(_("Time logs are required for {0} {1}") .format(bold("Job Card"), get_link_to_form("Job Card", self.name))) @@ -299,6 +336,10 @@ class JobCard(Document): if not self.work_order: return + if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings', + 'add_corrective_operation_cost_in_finished_good_valuation')): + return + for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] @@ -346,8 +387,8 @@ class JobCard(Document): min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE - jctl.parent = jc.name and jc.work_order = %s - and jc.operation_id = %s and jc.docstatus = 1 + jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s + and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 """, (self.work_order, self.operation_id), as_dict=1) for data in wo.operations: @@ -453,9 +494,11 @@ class JobCard(Document): .format(bold(self.operation), work_order), OperationMismatchError) def validate_sequence_id(self): - if self.is_corrective_job_card: return + if self.is_corrective_job_card: + return - if not (self.work_order and self.sequence_id): return + if not (self.work_order and self.sequence_id): + return current_operation_qty = 0.0 data = self.get_current_operation_data() @@ -480,7 +523,7 @@ class JobCard(Document): @frappe.whitelist() def make_time_log(args): - if isinstance(args, string_types): + if isinstance(args, str): args = json.loads(args) args = frappe._dict(args) @@ -632,6 +675,8 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.for_operation = for_operation target.set('time_logs', []) + target.set('employee', []) + target.set('items', []) target.get_sub_operations() target.get_required_items() target.validate_time_logs() diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index a239a247e3..d91530dd3b 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -17,8 +17,6 @@ "required_qty", "column_break_9", "transferred_qty", - "rate", - "amount", "allow_alternative_item" ], "fields": [ @@ -27,8 +25,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item", - "read_only": 1 + "options": "Item" }, { "fieldname": "source_warehouse", @@ -69,8 +66,7 @@ "fieldname": "required_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Required Qty", - "read_only": 1 + "label": "Required Qty" }, { "fieldname": "column_break_9", @@ -102,25 +98,14 @@ "fieldtype": "Float", "label": "Transferred Qty", "no_copy": 1, - "print_hide": 1 - }, - { - "fieldname": "rate", - "fieldtype": "Currency", - "label": "Rate", - "read_only": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount", + "print_hide": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-11 13:50:13.804108", + "modified": "2021-04-22 18:50:00.003444", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Item", diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json index be8190236d..9a8692b84d 100644 --- a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json +++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json @@ -7,7 +7,8 @@ "field_order": [ "sub_operation", "completed_time", - "status" + "status", + "completed_qty" ], "fields": [ { @@ -34,12 +35,18 @@ "label": "Operation", "options": "Operation", "read_only": 1 + }, + { + "fieldname": "completed_qty", + "fieldtype": "Float", + "label": "Completed Qty", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-14 17:08:25.992957", + "modified": "2021-03-16 18:24:35.399593", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Operation", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 6647be54eb..024f784725 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -27,6 +27,7 @@ "overproduction_percentage_for_work_order", "other_settings_section", "update_bom_costs_automatically", + "add_corrective_operation_cost_in_finished_good_valuation", "column_break_23", "make_serial_no_batch_from_work_order" ], @@ -168,13 +169,19 @@ "fieldname": "make_serial_no_batch_from_work_order", "fieldtype": "Check", "label": "Make Serial No / Batch from Work Order" + }, + { + "default": "0", + "fieldname": "add_corrective_operation_cost_in_finished_good_valuation", + "fieldtype": "Check", + "label": "Add Corrective Operation Cost in Finished Good Valuation" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-08 13:37:40.325838", + "modified": "2021-03-16 15:54:38.967341", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js index 9bfcc6eedb..102b6780e5 100644 --- a/erpnext/manufacturing/doctype/operation/operation.js +++ b/erpnext/manufacturing/doctype/operation/operation.js @@ -2,5 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Operation', { - + setup: function(frm) { + frm.set_query('operation', 'sub_operations', function() { + return { + filters: { + 'name': ['not in', [frm.doc.name]] + } + }; + }); + } }); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py index aaf0d5c01b..374f32019b 100644 --- a/erpnext/manufacturing/doctype/operation/operation.py +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt from frappe.model.document import Document class Operation(Document): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index acb3407e2b..512048512e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -189,35 +189,41 @@ frappe.ui.form.on("Work Order", { const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'), fields: [ { - fieldtype:'Link', - fieldname:'operation', + fieldtype: 'Link', + fieldname: 'operation', label: __('Operation'), - read_only:1, - in_list_view:1 + read_only: 1, + in_list_view: 1 }, { - fieldtype:'Link', - fieldname:'workstation', + fieldtype: 'Link', + fieldname: 'workstation', label: __('Workstation'), - read_only:1, - in_list_view:1 + read_only: 1, + in_list_view: 1 }, { - fieldtype:'Data', - fieldname:'name', + fieldtype: 'Data', + fieldname: 'name', label: __('Operation Id') }, { - fieldtype:'Float', - fieldname:'pending_qty', + fieldtype: 'Float', + fieldname: 'pending_qty', label: __('Pending Qty'), }, { - fieldtype:'Float', - fieldname:'qty', + fieldtype: 'Float', + fieldname: 'qty', label: __('Quantity to Manufacture'), - read_only:0, - in_list_view:1, + read_only: 0, + in_list_view: 1, + }, + { + fieldtype: 'Float', + fieldname: 'batch_size', + label: __('Batch Size'), + read_only: 1 }, ], data: operations_data, @@ -228,9 +234,13 @@ frappe.ui.form.on("Work Order", { }, function(data) { frappe.call({ method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", + freeze: true, args: { work_order: frm.doc.name, operations: data.operations, + }, + callback: function() { + frm.reload_doc(); } }); }, __("Job Card"), __("Create")); @@ -247,6 +257,7 @@ frappe.ui.form.on("Work Order", { 'name': data.name, 'operation': data.operation, 'workstation': data.workstation, + 'batch_size': data.batch_size, 'qty': pending_qty, 'pending_qty': pending_qty }); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 8e99c665f1..44d76d2b01 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -527,7 +527,8 @@ "depends_on": "has_serial_no", "fieldname": "serial_no", "fieldtype": "Small Text", - "label": "Serial Nos" + "label": "Serial Nos", + "no_copy": 1 }, { "default": "0", @@ -552,7 +553,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-03-16 13:27:51.116484", + "modified": "2021-06-20 15:19:14.902699", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c83f539e03..e343ed2dd3 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -27,9 +27,8 @@ class CapacityError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass -class SerialNoQtyError(frappe.ValidationError): pass - -from six import string_types +class SerialNoQtyError(frappe.ValidationError): + pass form_grid_templates = { "operations": "templates/form_grid/work_order_grid.html" @@ -248,7 +247,7 @@ class WorkOrder(Document): frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) - + if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): self.update_work_order_qty_in_combined_so() else: @@ -268,7 +267,7 @@ class WorkOrder(Document): self.update_work_order_qty_in_combined_so() else: self.update_work_order_qty_in_so() - + self.delete_job_card() self.update_completed_qty_in_material_request() self.update_planned_qty() @@ -277,10 +276,11 @@ class WorkOrder(Document): self.delete_auto_created_batch_and_serial_no() def create_serial_no_batch_no(self): - if not (self.has_serial_no or self.has_batch_no): return + if not (self.has_serial_no or self.has_batch_no): + return - if not cint(frappe.db.get_single_value("Manufacturing Settings", - "make_serial_no_batch_from_work_order")): return + if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + return if self.has_batch_no: self.create_batch_for_finished_good() @@ -346,29 +346,17 @@ class WorkOrder(Document): for index, row in enumerate(self.operations): qty = self.qty - i=0 while qty > 0: - i += 1 - if not cint(frappe.db.get_value("Operation", - row.operation, "create_job_card_based_on_batch_size")): - row.batch_size = self.qty - - job_card_qty = row.batch_size - if row.batch_size and qty >= row.batch_size: - qty -= row.batch_size - elif qty > 0: - job_card_qty = qty - qty = 0 - - if job_card_qty > 0: - self.prepare_data_for_job_card(row, job_card_qty, index, + qty = split_qty_based_on_batch_size(self, row, qty) + if row.job_card_qty > 0: + self.prepare_data_for_job_card(row, index, plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: self.db_set("planned_end_date", planned_end_date) - def prepare_data_for_job_card(self, row, job_card_qty, index, plan_days, enable_capacity_planning): + def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): self.set_operation_start_end_time(index, row) if not row.workstation: @@ -376,8 +364,8 @@ class WorkOrder(Document): .format(row.idx, row.operation)) original_start_time = row.planned_start_time - job_card_doc = create_job_card(self, row, qty=job_card_qty, - enable_capacity_planning=enable_capacity_planning, auto_create=True) + job_card_doc = create_job_card(self, row, auto_create=True, + enable_capacity_planning=enable_capacity_planning) if enable_capacity_planning and job_card_doc: row.planned_start_time = job_card_doc.time_logs[-1].from_time @@ -456,7 +444,7 @@ class WorkOrder(Document): work_order_qty = qty[0][0] if qty and qty[0][0] else 0 frappe.db.set_value('Sales Order Item', self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) - + def update_work_order_qty_in_combined_so(self): total_bundle_qty = 1 if self.product_bundle_item: @@ -469,7 +457,7 @@ class WorkOrder(Document): prod_plan = frappe.get_doc('Production Plan', self.production_plan) item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') - + for plan_reference in prod_plan.prod_plan_references: work_order_qty = 0.0 if plan_reference.item_reference == item_reference: @@ -477,7 +465,7 @@ class WorkOrder(Document): work_order_qty = flt(plan_reference.qty) / total_bundle_qty frappe.db.set_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty', work_order_qty) - + def update_completed_qty_in_material_request(self): if self.material_request: frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) @@ -761,8 +749,8 @@ class WorkOrder(Document): return bom def update_batch_produced_qty(self, stock_entry_doc): - if not cint(frappe.db.get_single_value("Manufacturing Settings", - "make_serial_no_batch_from_work_order")): return + if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + return for row in stock_entry_doc.items: if row.batch_no and (row.is_finished_item or row.is_scrap_item): @@ -848,7 +836,7 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): return wo_doc def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): - if isinstance(variant_items, string_types): + if isinstance(variant_items, str): variant_items = json.loads(variant_items) for item in variant_items: @@ -970,13 +958,47 @@ def query_sales_order(production_item): @frappe.whitelist() def make_job_card(work_order, operations): - if isinstance(operations, string_types): + if isinstance(operations, str): operations = json.loads(operations) work_order = frappe.get_doc('Work Order', work_order) for row in operations: + row = frappe._dict(row) validate_operation_data(row) - create_job_card(work_order, row, row.get("qty"), auto_create=True) + qty = row.get("qty") + while qty > 0: + qty = split_qty_based_on_batch_size(work_order, row, qty) + if row.job_card_qty > 0: + create_job_card(work_order, row, auto_create=True) + +def split_qty_based_on_batch_size(wo_doc, row, qty): + if not cint(frappe.db.get_value("Operation", + row.operation, "create_job_card_based_on_batch_size")): + row.batch_size = row.get("qty") or wo_doc.qty + + row.job_card_qty = row.batch_size + if row.batch_size and qty >= row.batch_size: + qty -= row.batch_size + elif qty > 0: + row.job_card_qty = qty + qty = 0 + + get_serial_nos_for_job_card(row, wo_doc) + + return qty + +def get_serial_nos_for_job_card(row, wo_doc): + if not wo_doc.serial_no: + return + + serial_nos = get_serial_nos(wo_doc.serial_no) + used_serial_nos = [] + for d in frappe.get_all('Job Card', fields=['serial_no'], + filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}): + used_serial_nos.extend(get_serial_nos(d.serial_no)) + + serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) + row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty]) def validate_operation_data(row): if row.get("qty") <= 0: @@ -995,21 +1017,22 @@ def validate_operation_data(row): ) ) -def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): +def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") doc.update({ 'work_order': work_order.name, 'operation': row.get("operation"), 'workstation': row.get("workstation"), 'posting_date': nowdate(), - 'for_quantity': qty or work_order.get('qty', 0), + 'for_quantity': row.job_card_qty or work_order.get('qty', 0), 'operation_id': row.get("name"), 'bom_no': work_order.bom_no, 'project': work_order.project, 'company': work_order.company, 'sequence_id': row.get("sequence_id"), 'wip_warehouse': work_order.wip_warehouse, - "hour_rate": row.get("hour_rate") + 'hour_rate': row.get("hour_rate"), + 'serial_no': row.get("serial_no") }) if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js index ef77566389..97e7e0a7d2 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js @@ -65,5 +65,41 @@ frappe.query_reports["Cost of Poor Quality Report"] = { fieldtype: "Link", options: "Workstation" }, + { + label: __("Item"), + fieldname: "production_item", + fieldtype: "Link", + options: "Item" + }, + { + label: __("Serial No"), + fieldname: "serial_no", + fieldtype: "Link", + options: "Serial No", + depends_on: "eval: doc.production_item", + get_query: function() { + var item_code = frappe.query_report.get_filter_value('production_item'); + return { + filters: { + item_code: item_code + } + } + } + }, + { + label: __("Batch No"), + fieldname: "batch_no", + fieldtype: "Link", + options: "Batch No", + depends_on: "eval: doc.production_item", + get_query: function() { + var item_code = frappe.query_report.get_filter_value('production_item'); + return { + filters: { + item: item_code + } + } + } + }, ] }; diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 2e8c191c60..9f81e7d26a 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -20,7 +20,7 @@ def get_data(report_filters): if operations: operations = [d.name for d in operations] fields = ["production_item as item_code", "item_name", "work_order", "operation", - "workstation", "total_time_in_mins", "name", "hour_rate"] + "workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"] filters = get_filters(report_filters, operations) @@ -30,15 +30,18 @@ def get_data(report_filters): for row in job_cards: row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) update_raw_material_cost(row, report_filters) - update_time_details(row, report_filters, data) + data.append(row) return data def get_filters(report_filters, operations): filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} - for field in ["name", "work_order", "operation", "workstation", "company"]: + for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]: if report_filters.get(field): - filters[field] = report_filters.get(field) + if field != 'serial_no': + filters[field] = report_filters.get(field) + else: + filters[field] = ('like', '% {} %'.format(report_filters.get(field))) return filters @@ -48,24 +51,6 @@ def update_raw_material_cost(row, filters): filters={"parent": row.name, "docstatus": 1}): row.rm_cost += data.amount -def update_time_details(row, filters, data): - args = frappe._dict({"item_code": "", "item_name": "", "name": "", "work_order":"", - "operation": "", "workstation":"", "operating_cost": "", "rm_cost": "", "total_time_in_mins": ""}) - - i=0 - for time_log in frappe.get_all("Job Card Time Log", - fields = ["from_time", "to_time", "time_in_mins"], - filters={"parent": row.name, "docstatus": 1, - "from_time": (">=", filters.from_date), "to_time": ("<=", filters.to_date)}): - - if i==0: - i += 1 - row.update(time_log) - data.append(row) - else: - args.update(time_log) - data.append(args) - def get_columns(filters): return [ { @@ -102,6 +87,18 @@ def get_columns(filters): "options": "Operation", "width": "100" }, + { + "label": _("Serial No"), + "fieldtype": "Data", + "fieldname": "serial_no", + "width": "100" + }, + { + "label": _("Batch No"), + "fieldtype": "Data", + "fieldname": "batch_no", + "width": "100" + }, { "label": _("Workstation"), "fieldtype": "Link", @@ -126,23 +123,5 @@ def get_columns(filters): "fieldtype": "Float", "fieldname": "total_time_in_mins", "width": "100" - }, - { - "label": _("From Time"), - "fieldtype": "Datetime", - "fieldname": "from_time", - "width": "100" - }, - { - "label": _("To Time"), - "fieldtype": "Datetime", - "fieldname": "to_time", - "width": "100" - }, - { - "label": _("Time in Mins"), - "fieldtype": "Float", - "fieldname": "time_in_mins", - "width": "100" - }, + } ] \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e49c9a57c3..8f27ef4356 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1097,7 +1097,8 @@ class StockEntry(StockController): def set_batchwise_finished_goods(self, args, item): qty = flt(self.fg_completed_qty) - filters = {"reference_name": self.pro_doc.name, + filters = { + "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, "qty_to_produce": (">", 0) } @@ -1106,7 +1107,8 @@ class StockEntry(StockController): for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): batch_qty = flt(row.qty) - flt(row.produced_qty) - if not batch_qty: continue + if not batch_qty: + continue if qty <=0: break @@ -1701,6 +1703,10 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if bom.quantity: operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) + if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings', + 'add_corrective_operation_cost_in_finished_good_valuation')): + operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) + return operating_cost_per_unit def get_used_alternative_items(purchase_order=None, work_order=None): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 864ff488b2..a178283904 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -18,6 +18,7 @@ "col_break2", "is_finished_item", "is_scrap_item", + "quality_inspection", "subcontracted_item", "section_break_8", "description", @@ -69,7 +70,6 @@ "putaway_rule", "column_break_51", "reference_purchase_receipt", - "quality_inspection", "job_card_item" ], "fields": [ @@ -548,7 +548,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-11 13:47:50.158754", + "modified": "2021-04-22 20:08:23.799715", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From 9382b1f154502cdfc50be75042bc270d7aae3eb8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:03:22 +0530 Subject: [PATCH 231/550] fix: Flaky test --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ff433b962f..2f5d36c8fa 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -966,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase): update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) # Create Purchase Order with TDS applied - po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000) + po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item') po.apply_tds = 1 po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() @@ -1002,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Invoice against Purchase Order purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice.allocate_advances_automatically = 1 + purchase_invoice.items[0].item_code = '_Test Non Stock Item' purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' purchase_invoice.save() purchase_invoice.submit() From e21e435a0d5b2aa6c6433233d499e09b63139f03 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:17:58 +0530 Subject: [PATCH 232/550] fix: Add python 3 compatible string types --- erpnext/public/js/controllers/taxes_and_totals.js | 4 ++-- erpnext/stock/get_item_details.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 4a14a665cd..3f76a3e927 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -288,8 +288,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ company: me.frm.doc.company, tax_category: cstr(me.frm.doc.tax_category), item_codes: item_codes, - item_tax_templates: item_tax_templates, - item_rates: item_rates + item_rates: item_rates, + item_tax_templates: item_tax_templates }, callback: function(r) { if (!r.exc) { diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 37850350ab..c64084fe34 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -436,15 +436,15 @@ def get_barcode_data(items_list): return itemwise_barcode @frappe.whitelist() -def get_item_tax_info(company, tax_category, item_codes, item_tax_templates=None, item_rates=None): +def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if isinstance(item_codes, string_types): + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) - if isinstance(item_rates, string_types): + if isinstance(item_rates, (str,)): item_rates = json.loads(item_rates) - if isinstance(item_tax_templates, string_types): + if isinstance(item_tax_templates, (str,)): item_tax_templates = json.loads(item_tax_templates) for item_code in item_codes: @@ -514,7 +514,7 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return None # do not change if already a valid template - if args.get('item_tax_template') in [t.item_tax_template for t in taxes]: + if args.get('item_tax_template') in {t.item_tax_template for t in taxes}: out["item_tax_template"] = args.get('item_tax_template') return args.get('item_tax_template') From 1658107a926ee1880a79581158e0dd8205ae5f9f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:18:50 +0530 Subject: [PATCH 233/550] fix: Linting fixes --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 3f76a3e927..1de9ec1a7d 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -277,7 +277,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ // Use combination of name and item code in case same item is added multiple times item_codes.push([item.item_code, item.name]); item_rates[item.name] = item.net_rate; - item_tax_templates[item.name] = item.item_tax_template + item_tax_templates[item.name] = item.item_tax_template; } }); From b3a0a7b4329aa1574a6d42abf61bf8691b8f8145 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Jun 2021 19:30:10 +0530 Subject: [PATCH 234/550] fix: too many writes while renaming company abbreviation (#26203) --- erpnext/setup/doctype/company/company.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 27e023c1e5..0427abe558 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -407,8 +407,6 @@ def replace_abbr(company, old, new): frappe.only_for("System Manager") - frappe.db.set_value("Company", company, "abbr", new) - def _rename_record(doc): parts = doc[0].rsplit(" - ", 1) if len(parts) == 1 or parts[1].lower() == old.lower(): @@ -419,11 +417,18 @@ def replace_abbr(company, old, new): doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company)) for d in doc: _rename_record(d) + try: + frappe.db.auto_commit_on_many_writes = 1 + frappe.db.set_value("Company", company, "abbr", new) + for dt in ["Warehouse", "Account", "Cost Center", "Department", + "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: + _rename_records(dt) + frappe.db.commit() - for dt in ["Warehouse", "Account", "Cost Center", "Department", - "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: - _rename_records(dt) - frappe.db.commit() + except Exception: + frappe.log_error(title=_('Abbreviation Rename Error')) + finally: + frappe.db.auto_commit_on_many_writes = 0 def get_name_with_abbr(name, company): From 5708b7140b45db45d3239f9cf1407cdaf89b6eae Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 24 Jun 2021 19:38:37 +0530 Subject: [PATCH 235/550] fix: batch nos in packed items (bp #26105) * test: batch info in packed_items * fix(ux): make packed items editable * refactor: allow custom table name for set_batch In some doctypes there are multiple child tables requiring batched items. This change makes the function a bit more flexible. * fix: Auto fetch batch_nos in packed_item table --- erpnext/stock/doctype/batch/batch.py | 4 +-- .../doctype/delivery_note/delivery_note.js | 3 ++ .../doctype/delivery_note/delivery_note.json | 5 ++-- .../doctype/delivery_note/delivery_note.py | 7 +++-- .../delivery_note/test_delivery_note.py | 29 +++++++++++++++---- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index bb5ad5c6fe..cd441b5958 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -226,9 +226,9 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): return batch.name -def set_batch_nos(doc, warehouse_field, throw=False): +def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" - for d in doc.items: + for d in doc.get(child_table): qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no') warehouse = d.get(warehouse_field, None) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 7875b9cd87..74cb3fcb1f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -78,6 +78,9 @@ frappe.ui.form.on("Delivery Note", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + + frm.set_df_property('packed_items', 'cannot_add_rows', true); + frm.set_df_property('packed_items', 'cannot_delete_rows', true); }, print_without_amount: function(frm) { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 280fde158f..f20e76f5bf 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -554,8 +554,7 @@ "oldfieldname": "packing_details", "oldfieldtype": "Table", "options": "Packed Item", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "product_bundle_help", @@ -1289,7 +1288,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-04-15 23:55:49.620641", + "modified": "2021-06-11 19:27:30.901112", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index dd31965fac..fcdb5f3b19 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -129,12 +129,13 @@ class DeliveryNote(SellingController): self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() - if self._action != 'submit' and not self.is_return: - set_batch_nos(self, 'warehouse', True) - from erpnext.stock.doctype.packed_item.packed_item import make_packing_list make_packing_list(self) + if self._action != 'submit' and not self.is_return: + set_batch_nos(self, 'warehouse', throw=True) + set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items") + self.update_current_stock() if not self.installation_status: self.installation_status = 'Not Installed' diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 0c63df0e22..f981aeb13b 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -7,7 +7,7 @@ import unittest import frappe import json import frappe.defaults -from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today +from frappe.utils import nowdate, nowtime, cstr, flt from erpnext.stock.stock_ledger import get_previous_sle from erpnext.accounts.utils import get_balance_on from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries @@ -18,9 +18,11 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, SerialNoWa from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \ import create_stock_reconciliation, set_valuation_method from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order, create_dn_against_so -from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account +from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + class TestDeliveryNote(unittest.TestCase): def test_over_billing_against_dn(self): @@ -277,8 +279,6 @@ class TestDeliveryNote(unittest.TestCase): dn.cancel() def test_sales_return_for_non_bundled_items_full(self): - from erpnext.stock.doctype.item.test_item import make_item - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') make_item("Box", {'is_stock_item': 1}) @@ -741,6 +741,25 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(si2.items[0].qty, 2) self.assertEqual(si2.items[1].qty, 1) + + def test_delivery_note_bundle_with_batched_item(self): + batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) + batched_item = make_item("_Test Batched Item", + {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"} + ) + make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) + make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42) + + try: + dn = create_delivery_note(item_code=batched_bundle.name, qty=1) + except frappe.ValidationError as e: + if "batch" in str(e).lower(): + self.fail("Batch numbers not getting added to bundled items in DN.") + raise e + + self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) From bdba29bab56491eefb26281a804974739d240e85 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:48:11 +0530 Subject: [PATCH 236/550] fix: Account filter not working with accounting dimension filter --- .../doctype/accounting_dimension/accounting_dimension.py | 2 +- erpnext/accounts/report/general_ledger/general_ledger.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 7cd1e7736c..fac28c9239 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -19,7 +19,7 @@ class AccountingDimension(Document): def validate(self): if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', - 'Cost Center', 'Accounting Dimension Detail', 'Company') : + 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') : msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type) frappe.throw(msg) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 03808c3640..914058e633 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -211,7 +211,7 @@ def get_gl_entries(filters, accounting_dimensions): dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, order_by_statement=order_by_statement ), - filters, as_dict=1) + filters, as_dict=1, debug=1) if filters.get('presentation_currency'): return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) @@ -222,7 +222,7 @@ def get_gl_entries(filters, accounting_dimensions): def get_conditions(filters): conditions = [] - if filters.get("account") and not filters.get("include_dimensions"): + if filters.get("account"): filters.account = get_accounts_with_children(filters.account) conditions.append("account in %(account)s") From e3ca1778281c3e6d409741d39f641ea2f7cbbb16 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 19:51:23 +0530 Subject: [PATCH 237/550] fix: Remove debug flag --- erpnext/accounts/report/general_ledger/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 914058e633..744ada9e55 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -211,7 +211,7 @@ def get_gl_entries(filters, accounting_dimensions): dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, order_by_statement=order_by_statement ), - filters, as_dict=1, debug=1) + filters, as_dict=1) if filters.get('presentation_currency'): return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) From 21e44b19f0459286d3750cbb561339af3de0050f Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 24 Jun 2021 20:58:07 +0530 Subject: [PATCH 238/550] perf: don't query unless required (bp #26175) re-order conditionals so queries are not evaluated unless required. # Conflicts: # erpnext/stock/doctype/batch/batch.py --- erpnext/controllers/sales_and_purchase_return.py | 9 +++++---- erpnext/stock/doctype/batch/batch.py | 5 ++--- erpnext/stock/doctype/delivery_note/delivery_note.py | 5 ++--- .../stock/doctype/material_request/material_request.py | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5f759b43bc..80ccc6d75b 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -99,9 +99,10 @@ def validate_returned_items(doc): frappe.throw(_("Row # {0}: Serial No {1} does not match with {2} {3}") .format(d.idx, s, doc.doctype, doc.return_against)) - if warehouse_mandatory and frappe.db.get_value("Item", d.item_code, "is_stock_item") \ - and not d.get("warehouse"): - frappe.throw(_("Warehouse is mandatory")) + if (warehouse_mandatory and not d.get("warehouse") and + frappe.db.get_value("Item", d.item_code, "is_stock_item") + ): + frappe.throw(_("Warehouse is mandatory")) items_returned = True @@ -462,4 +463,4 @@ def get_returned_serial_nos(child_doc, parent_doc): for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters): serial_nos.extend(get_serial_nos(row.serial_no)) - return serial_nos \ No newline at end of file + return serial_nos diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index cd441b5958..b6eef6ca48 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -230,9 +230,8 @@ def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" for d in doc.get(child_table): qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 - has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no') warehouse = d.get(warehouse_field, None) - if has_batch_no and warehouse and qty > 0: + if warehouse and qty > 0 and frappe.db.get_value('Item', d.item_code, 'has_batch_no'): if not d.batch_no: d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) else: @@ -313,4 +312,4 @@ def validate_serial_no_with_batch(serial_nos, item_code): def make_batch(args): if frappe.db.get_value("Item", args.item, "has_batch_no"): args.doctype = "Batch" - frappe.get_doc(args).insert().name \ No newline at end of file + frappe.get_doc(args).insert().name diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index fcdb5f3b19..4808e948fc 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -182,9 +182,8 @@ class DeliveryNote(SellingController): super(DeliveryNote, self).validate_warehouse() for d in self.get_item_list(): - if frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: - if not d['warehouse']: - frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) + if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: + frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) def update_current_stock(self): diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 335175f21d..3ad9909ad0 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -189,7 +189,7 @@ class MaterialRequest(BuyingController): item_wh_list = [] for d in self.get("items"): if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \ - and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 and d.warehouse: + and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 : item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: From 8958f5589023284b963ad45e66e7930de74e6128 Mon Sep 17 00:00:00 2001 From: Alan <2.alan.tom@gmail.com> Date: Thu, 24 Jun 2021 21:01:55 +0530 Subject: [PATCH 239/550] fix: add validation for 'for_qty' else throws errors (#25829) * fix: add validation for 'for_qty' else throws errors * fix: check if for_qty is None * fix: check purpose * fix: add purpose to pick list get_doc * fix: set as read only to prevent se from picking up value Co-authored-by: Ankush * chore: undo changes to doctype modified timestamp Co-authored-by: Ankush --- erpnext/stock/doctype/pick_list/pick_list.json | 2 +- erpnext/stock/doctype/pick_list/pick_list.py | 9 +++++++++ erpnext/stock/doctype/pick_list/test_pick_list.py | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index c01388dcd2..2146793537 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -184,4 +184,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 6ab68e292a..e795742ea4 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -17,6 +17,9 @@ from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note a # TODO: Prioritize SO or WO group warehouse class PickList(Document): + def validate(self): + self.validate_for_qty() + def before_save(self): self.set_item_locations() @@ -35,6 +38,7 @@ class PickList(Document): @frappe.whitelist() def set_item_locations(self, save=False): + self.validate_for_qty() items = self.aggregate_item_qty() self.item_location_map = frappe._dict() @@ -107,6 +111,11 @@ class PickList(Document): return item_map.values() + def validate_for_qty(self): + if self.purpose == "Material Transfer for Manufacture" \ + and (self.for_qty is None or self.for_qty == 0): + frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) + def validate_item_locations(pick_list): if not pick_list.locations: diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index c4da05a6d4..84566b8d8c 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -37,6 +37,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item', 'qty': 5, @@ -90,6 +91,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item Warehouse Group Wise Reorder', 'qty': 1000, @@ -135,6 +137,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Serialized Item', 'qty': 1000, @@ -264,6 +267,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item', 'qty': 5, @@ -319,6 +323,7 @@ class TestPickList(unittest.TestCase): 'company': '_Test Company', 'customer': '_Test Customer', 'items_based_on': 'Sales Order', + 'purpose': 'Delivery', 'locations': [{ 'item_code': '_Test Item', 'qty': 1, From f4e8e7d933fc7d96d22da7e23fe22f9bb6cba200 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:18:59 +0530 Subject: [PATCH 240/550] fix(Asset): Remove extra tabs --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 29379657a1..95e3c8ad71 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -196,7 +196,7 @@ class Asset(AccountsController): if has_pro_rata: number_of_pending_depreciations += 1 - + skip_row = False for n in range(start, number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) From 289b449f2bf11e79fef5501bc5e012f0e589e416 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:21:08 +0530 Subject: [PATCH 241/550] fix(Asset): Add comment --- erpnext/assets/doctype/asset/asset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 95e3c8ad71..66f0bdcd58 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -224,6 +224,7 @@ class Asset(AccountsController): # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: if not self.flags.increase_in_asset_life: + # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months(self.available_for_use_date, n * cint(d.frequency_of_depreciation)) From b44e4121da48debd9b0481d558625afca781e150 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:25:45 +0530 Subject: [PATCH 242/550] fix(Asset Repair): Move filters for cost_center, warehouse and project to setup --- .../doctype/asset_repair/asset_repair.js | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 91bed4fdd0..1cebfff66e 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -2,6 +2,34 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Repair', { + setup: function(frm) { + frm.fields_dict.cost_center.get_query = function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }; + + frm.fields_dict.project.get_query = function(doc) { + return { + filters: { + 'company': doc.company + } + }; + }; + + frm.fields_dict.warehouse.get_query = function(doc) { + return { + filters: { + 'is_group': 0, + 'company': doc.company + } + }; + }; + }, + refresh: function(frm) { if (frm.doc.docstatus) { frm.add_custom_button("View General Ledger", function() { @@ -40,30 +68,4 @@ frappe.ui.form.on('Asset Repair Consumed Item', { var row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); }, -}); - -cur_frm.fields_dict.cost_center.get_query = function(doc) { - return { - filters: { - 'is_group': 0, - 'company': doc.company - } - }; -}; - -cur_frm.fields_dict.project.get_query = function(doc) { - return { - filters: { - 'company': doc.company - } - }; -}; - -cur_frm.fields_dict.warehouse.get_query = function(doc) { - return { - filters: { - 'is_group': 0, - 'company': doc.company - } - }; -}; \ No newline at end of file +}); \ No newline at end of file From c73e137bfa934de13b2512a44243ba810f123553 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:27:30 +0530 Subject: [PATCH 243/550] fix(Asset Repair): Edit description for total_repair_cost --- erpnext/assets/doctype/asset_repair/asset_repair.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 6a14384f3f..0651deb2ac 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -212,7 +212,7 @@ }, { "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0", - "description": "Sum of Repair Cost and the total value of all Stock Items consumed during the repair.", + "description": "Sum of Repair Cost and Value of Consumed Stock Items.", "fieldname": "total_repair_cost", "fieldtype": "Currency", "label": "Total Repair Cost", From dcb8a2895cee137054656487005706007bf6691f Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 24 Jun 2021 22:30:08 +0530 Subject: [PATCH 244/550] fix(Asset Repair): Simplify code for Asset Repair creation in tests --- erpnext/assets/doctype/asset_repair/test_asset_repair.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index b3d78b3bfb..30bbb37851 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -138,10 +138,7 @@ def create_asset_repair(**args): "consumed_quantity": args.qty or 1 }) - try: - asset_repair.save() - except frappe.DuplicateEntryError: - pass + asset_repair.insert(ignore_if_duplicate=True) if args.submit: asset_repair.repair_status = "Completed" From 43f85a2877eddd6f1920038e07ba477ab4c5b4c6 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Fri, 25 Jun 2021 11:38:11 +0530 Subject: [PATCH 245/550] fix: changed profitability analysis report width (#26165) --- .../profitability_analysis/profitability_analysis.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 60e675f2f1..48bd7308bc 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -168,21 +168,24 @@ def get_columns(filters): "label": _("Income"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 305 + }, { "fieldname": "expense", "label": _("Expense"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 305 + }, { "fieldname": "gross_profit_loss", "label": _("Gross Profit / Loss"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 307 + } ] From 58daf5f91652056027ae19990e0f20407775d518 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 25 Jun 2021 12:52:17 +0530 Subject: [PATCH 246/550] fix: precision rate for packed items (#26046) --- erpnext/controllers/selling_controller.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7f28289760..da2765dede 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -330,9 +330,15 @@ class SellingController(StockController): # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): - rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) - if d.rate != rate: - d.rate = rate + if d.doctype == "Packed Item": + incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate')) + if d.incoming_rate != incoming_rate: + d.incoming_rate = incoming_rate + else: + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 d.discount_amount = 0 frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") From 532a224c4456f0fe8bb9805d76d4211d2af79613 Mon Sep 17 00:00:00 2001 From: Ankush Date: Fri, 25 Jun 2021 13:28:01 +0530 Subject: [PATCH 247/550] fix: precision rate for packed items (#26046) (#26217) Co-authored-by: Noah Jacob --- erpnext/controllers/selling_controller.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7f28289760..da2765dede 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -330,9 +330,15 @@ class SellingController(StockController): # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): - rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) - if d.rate != rate: - d.rate = rate + if d.doctype == "Packed Item": + incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate')) + if d.incoming_rate != incoming_rate: + d.incoming_rate = incoming_rate + else: + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 d.discount_amount = 0 frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") From 2ca9b765efb6f96c86fbf1cd5e284c54b77ae4ba Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:34:00 +0530 Subject: [PATCH 248/550] fix: Error while fetching item taxes --- erpnext/stock/get_item_details.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c64084fe34..e0a0c4a472 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -438,6 +438,10 @@ def get_barcode_data(items_list): @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} + + if not item_tax_templates: + item_tax_templates = {} + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) From e955a4ee72d2f20041c966defc3389b3b98483e7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:38:06 +0530 Subject: [PATCH 249/550] fix: Check for is None --- erpnext/stock/get_item_details.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e0a0c4a472..ca174a3f63 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -439,8 +439,11 @@ def get_barcode_data(items_list): def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if not item_tax_templates: + if item_tax_templates is None: item_tax_templates = {} + + if item_rates is None: + item_rates = {} if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) @@ -457,7 +460,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) From cd36ba7e64343c6997a5aa710196af63fea573fc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:34:00 +0530 Subject: [PATCH 250/550] fix: Error while fetching item taxes --- erpnext/stock/get_item_details.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c64084fe34..e0a0c4a472 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -438,6 +438,10 @@ def get_barcode_data(items_list): @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} + + if not item_tax_templates: + item_tax_templates = {} + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) From 6eb8d19cc9e3e263a90cde4fb1cf7f0abae21f21 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:38:06 +0530 Subject: [PATCH 251/550] fix: Check for is None --- erpnext/stock/get_item_details.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e0a0c4a472..ca174a3f63 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -439,8 +439,11 @@ def get_barcode_data(items_list): def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if not item_tax_templates: + if item_tax_templates is None: item_tax_templates = {} + + if item_rates is None: + item_rates = {} if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) @@ -457,7 +460,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) From ff4aadc6577b2f97e0e4fbe8413ca404a195ed36 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 25 May 2021 17:40:59 +0530 Subject: [PATCH 252/550] chore: remove dead and py2 compatibility code form_grid_template doesn't exist --- erpnext/manufacturing/doctype/work_order/work_order.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index e343ed2dd3..302753214b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1,7 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe import json import math @@ -30,9 +29,6 @@ class ItemHasVariantError(frappe.ValidationError): pass class SerialNoQtyError(frappe.ValidationError): pass -form_grid_templates = { - "operations": "templates/form_grid/work_order_grid.html" -} class WorkOrder(Document): def onload(self): From 6ec8875434a18f6b3a0dbb2698e53f7c06c5abdc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 14:47:44 +0530 Subject: [PATCH 253/550] fix(ux): show bom in operations child table --- .../work_order_operation/work_order_operation.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 6d8fb80e31..f7b8787a0b 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -2,7 +2,6 @@ "actions": [], "creation": "2014-10-16 14:35:41.950175", "doctype": "DocType", - "editable_grid": 1, "engine": "InnoDB", "field_order": [ "details", @@ -49,6 +48,7 @@ { "fieldname": "bom", "fieldtype": "Link", + "in_list_view": 1, "label": "BOM", "no_copy": 1, "options": "BOM", @@ -68,6 +68,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "Operation completed for how many finished goods?", "fieldname": "completed_qty", "fieldtype": "Float", @@ -77,6 +78,7 @@ "read_only": 1 }, { + "columns": 1, "default": "Pending", "fieldname": "status", "fieldtype": "Select", @@ -119,6 +121,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "in Minutes", "fieldname": "time_in_mins", "fieldtype": "Float", @@ -205,7 +208,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-12 14:48:31.061286", + "modified": "2021-06-24 14:36:12.835543", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -214,4 +217,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 9e43445f3602ab322f629e800c4677b1a0da7f55 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 12:22:50 +0530 Subject: [PATCH 254/550] fix: order and time of operations for multilevel bom - Order of operations was being sorted by idx of individual operations in BOM table, which made the ordering useless. - This adds ordering that's sorted from lowest level item to top level item. - chore: remove dead functionality. There's no `items` table. Required item level operations get overwritten on fetching of items / operations e.g. when clicking on multi-level BOM checkbox. - test: add test for tree representation - feat: BOMTree class to get complete representation of a tree --- erpnext/manufacturing/doctype/bom/bom.py | 85 ++++++++++++++++++- erpnext/manufacturing/doctype/bom/test_bom.py | 82 +++++++++++++++++- .../doctype/work_order/work_order.py | 57 +++++++------ 3 files changed, 189 insertions(+), 35 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3e855603b4..c58f017258 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1,7 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from typing import List +from collections import deque import frappe, erpnext from frappe.utils import cint, cstr, flt, today from frappe import _ @@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc import functools -from six import string_types - from operator import itemgetter form_grid_templates = { "items": "templates/form_grid/item_grid.html" } + +class BOMTree: + """Full tree representation of a BOM""" + + # specifying the attributes to save resources + # ref: https://docs.python.org/3/reference/datamodel.html#slots + __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + + def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + self.name = name # name of node, BOM number if is_bom else item_code + self.child_items: List["BOMTree"] = [] # list of child items + self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.item_code: str = None # item_code associated with node + self.qty = qty # required unit quantity to make one unit of parent item. + self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. + if not self.is_bom: + self.item_code = self.name + else: + self.__create_tree() + + def __create_tree(self): + bom = frappe.get_cached_doc("BOM", self.name) + self.item_code = bom.item + + for item in bom.get("items", []): + qty = item.qty / bom.quantity # quantity per unit + exploded_qty = self.exploded_qty * qty + if item.bom_no: + child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) + self.child_items.append(child) + else: + self.child_items.append( + BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty) + ) + + def level_order_traversal(self) -> List["BOMTree"]: + """Get level order traversal of tree. + E.g. for following tree the traversal will return list of nodes in order from top to bottom. + BOM: + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 + + returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] + """ + traversal = [] + q = deque() + q.append(self) + + while q: + node = q.popleft() + + for child in node.child_items: + traversal.append(child) + q.append(child) + + return traversal + + def __str__(self) -> str: + return ( + f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}" + f" exploded_qty: {self.exploded_qty}" + ) + + def __repr__(self, level: int = 0) -> str: + rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n" + for child in self.child_items: + rep += child.__repr__(level=level + 1) + return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", @@ -152,7 +224,7 @@ class BOM(WebsiteGenerator): if not args: args = frappe.form_dict.get('args') - if isinstance(args, string_types): + if isinstance(args, str): import json args = json.loads(args) @@ -600,6 +672,11 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + def get_tree_representation(self) -> BOMTree: + """Get a complete tree representation preserving order of child items.""" + return BOMTree(self.name) + + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 1f443fb95a..57a5458726 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,14 +2,13 @@ # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from collections import deque import unittest import frappe from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost -from six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.tests.test_subcontracting import set_backflush_based_on @@ -227,11 +226,88 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + def test_bom_tree_representation(self): + bom_tree = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + created_tree = parent_bom.get_tree_representation() + + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + created_order = created_tree.level_order_traversal() + + self.assertEqual(len(reqd_order), len(created_order)) + + for reqd_item, created_item in zip(reqd_order, created_order): + self.assertEqual(reqd_item, created_item.item_code) + + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + + + +def level_order_traversal(node): + traversal = [] + q = deque() + q.append(node) + + while q: + node = q.popleft() + + for node_name, subtree in node.items(): + traversal.append(node_name) + q.append(subtree) + + return traversal + +def create_nested_bom(tree, prefix="_Test bom "): + """ Helper function to create a simple nested bom from tree describing item names. (along with required items) + """ + + def create_items(bom_tree): + for item_code, subtree in bom_tree.items(): + bom_item_code = prefix + item_code + if not frappe.db.exists("Item", bom_item_code): + frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() + create_items(subtree) + create_items(tree) + + def dfs(tree, node): + """naive implementation for searching right subtree""" + for node_name, subtree in tree.items(): + if node_name == node: + return subtree + else: + result = dfs(subtree, node) + if result is not None: + return result + + order_of_creating_bom = reversed(level_order_traversal(tree)) + + for item in order_of_creating_bom: + child_items = dfs(tree, item) + if child_items: + bom_item_code = prefix + item + bom = frappe.get_doc(doctype="BOM", item=bom_item_code) + for child_item in child_items.keys(): + bom.append("items", {"item_code": prefix + child_item}) + bom.insert() + bom.submit() + + return bom # parent bom is last bom + + def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): - if warehouse_list and isinstance(warehouse_list, string_types): + if warehouse_list and isinstance(warehouse_list, str): warehouse_list = [warehouse_list] if not warehouse_list: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 302753214b..180815d80e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -468,46 +468,47 @@ class WorkOrder(Document): def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" - self.set('operations', []) + def _get_operations(bom_no, qty=1): + return frappe.db.sql( + f"""select + operation, description, workstation, idx, + base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, + "Pending" as status, parent as bom, batch_size, sequence_id + from + `tabBOM Operation` + where + parent = %s order by idx + """, bom_no, as_dict=1) + + + self.set('operations', []) if not self.bom_no: return - if self.use_multi_level_bom: - bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() + operations = [] + if not self.use_multi_level_bom: + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) else: - bom_list = [self.bom_no] + bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() + bom_traversal = list(reversed(bom_tree.level_order_traversal())) + bom_traversal.append(bom_tree) # add operation on top level item last + + for d in bom_traversal: + if d.is_bom: + operations.extend(_get_operations(d.name, qty=d.exploded_qty)) + + for correct_index, operation in enumerate(operations, start=1): + operation.idx = correct_index - operations = frappe.db.sql(""" - select - operation, description, workstation, idx, - base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id - from - `tabBOM Operation` - where - parent in (%s) order by idx - """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) self.set('operations', operations) - - if self.use_multi_level_bom and self.get('operations') and self.get('items'): - raw_material_operations = [d.operation for d in self.get('items')] - operations = [d.operation for d in self.get('operations')] - - for operation in raw_material_operations: - if operation not in operations: - self.append('operations', { - 'operation': operation - }) - self.calculate_time() def calculate_time(self): - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() From b4e7ee0e45010bac5a783845cb15b46aec4d73c9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 25 May 2021 17:40:59 +0530 Subject: [PATCH 255/550] chore: remove dead and py2 compatibility code form_grid_template doesn't exist --- erpnext/manufacturing/doctype/work_order/work_order.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index e343ed2dd3..302753214b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1,7 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe import json import math @@ -30,9 +29,6 @@ class ItemHasVariantError(frappe.ValidationError): pass class SerialNoQtyError(frappe.ValidationError): pass -form_grid_templates = { - "operations": "templates/form_grid/work_order_grid.html" -} class WorkOrder(Document): def onload(self): From 9af3f12411cbdadb0611a10c2bfb4edef0b876ab Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 14:47:44 +0530 Subject: [PATCH 256/550] fix(ux): show bom in operations child table --- .../work_order_operation/work_order_operation.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 6d8fb80e31..f7b8787a0b 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -2,7 +2,6 @@ "actions": [], "creation": "2014-10-16 14:35:41.950175", "doctype": "DocType", - "editable_grid": 1, "engine": "InnoDB", "field_order": [ "details", @@ -49,6 +48,7 @@ { "fieldname": "bom", "fieldtype": "Link", + "in_list_view": 1, "label": "BOM", "no_copy": 1, "options": "BOM", @@ -68,6 +68,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "Operation completed for how many finished goods?", "fieldname": "completed_qty", "fieldtype": "Float", @@ -77,6 +78,7 @@ "read_only": 1 }, { + "columns": 1, "default": "Pending", "fieldname": "status", "fieldtype": "Select", @@ -119,6 +121,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "in Minutes", "fieldname": "time_in_mins", "fieldtype": "Float", @@ -205,7 +208,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-12 14:48:31.061286", + "modified": "2021-06-24 14:36:12.835543", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -214,4 +217,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 6588a936d5dd96f434ca3590ff8eb01ae3e594fa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 12:22:50 +0530 Subject: [PATCH 257/550] fix: order and time of operations for multilevel bom - Order of operations was being sorted by idx of individual operations in BOM table, which made the ordering useless. - This adds ordering that's sorted from lowest level item to top level item. - chore: remove dead functionality. There's no `items` table. Required item level operations get overwritten on fetching of items / operations e.g. when clicking on multi-level BOM checkbox. - test: add test for tree representation - feat: BOMTree class to get complete representation of a tree --- erpnext/manufacturing/doctype/bom/bom.py | 85 ++++++++++++++++++- erpnext/manufacturing/doctype/bom/test_bom.py | 82 +++++++++++++++++- .../doctype/work_order/work_order.py | 57 +++++++------ 3 files changed, 189 insertions(+), 35 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3e855603b4..c58f017258 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1,7 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from typing import List +from collections import deque import frappe, erpnext from frappe.utils import cint, cstr, flt, today from frappe import _ @@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc import functools -from six import string_types - from operator import itemgetter form_grid_templates = { "items": "templates/form_grid/item_grid.html" } + +class BOMTree: + """Full tree representation of a BOM""" + + # specifying the attributes to save resources + # ref: https://docs.python.org/3/reference/datamodel.html#slots + __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + + def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + self.name = name # name of node, BOM number if is_bom else item_code + self.child_items: List["BOMTree"] = [] # list of child items + self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.item_code: str = None # item_code associated with node + self.qty = qty # required unit quantity to make one unit of parent item. + self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. + if not self.is_bom: + self.item_code = self.name + else: + self.__create_tree() + + def __create_tree(self): + bom = frappe.get_cached_doc("BOM", self.name) + self.item_code = bom.item + + for item in bom.get("items", []): + qty = item.qty / bom.quantity # quantity per unit + exploded_qty = self.exploded_qty * qty + if item.bom_no: + child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) + self.child_items.append(child) + else: + self.child_items.append( + BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty) + ) + + def level_order_traversal(self) -> List["BOMTree"]: + """Get level order traversal of tree. + E.g. for following tree the traversal will return list of nodes in order from top to bottom. + BOM: + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 + + returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] + """ + traversal = [] + q = deque() + q.append(self) + + while q: + node = q.popleft() + + for child in node.child_items: + traversal.append(child) + q.append(child) + + return traversal + + def __str__(self) -> str: + return ( + f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}" + f" exploded_qty: {self.exploded_qty}" + ) + + def __repr__(self, level: int = 0) -> str: + rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n" + for child in self.child_items: + rep += child.__repr__(level=level + 1) + return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", @@ -152,7 +224,7 @@ class BOM(WebsiteGenerator): if not args: args = frappe.form_dict.get('args') - if isinstance(args, string_types): + if isinstance(args, str): import json args = json.loads(args) @@ -600,6 +672,11 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + def get_tree_representation(self) -> BOMTree: + """Get a complete tree representation preserving order of child items.""" + return BOMTree(self.name) + + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 1f443fb95a..57a5458726 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,14 +2,13 @@ # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from collections import deque import unittest import frappe from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost -from six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.tests.test_subcontracting import set_backflush_based_on @@ -227,11 +226,88 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + def test_bom_tree_representation(self): + bom_tree = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + created_tree = parent_bom.get_tree_representation() + + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + created_order = created_tree.level_order_traversal() + + self.assertEqual(len(reqd_order), len(created_order)) + + for reqd_item, created_item in zip(reqd_order, created_order): + self.assertEqual(reqd_item, created_item.item_code) + + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + + + +def level_order_traversal(node): + traversal = [] + q = deque() + q.append(node) + + while q: + node = q.popleft() + + for node_name, subtree in node.items(): + traversal.append(node_name) + q.append(subtree) + + return traversal + +def create_nested_bom(tree, prefix="_Test bom "): + """ Helper function to create a simple nested bom from tree describing item names. (along with required items) + """ + + def create_items(bom_tree): + for item_code, subtree in bom_tree.items(): + bom_item_code = prefix + item_code + if not frappe.db.exists("Item", bom_item_code): + frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() + create_items(subtree) + create_items(tree) + + def dfs(tree, node): + """naive implementation for searching right subtree""" + for node_name, subtree in tree.items(): + if node_name == node: + return subtree + else: + result = dfs(subtree, node) + if result is not None: + return result + + order_of_creating_bom = reversed(level_order_traversal(tree)) + + for item in order_of_creating_bom: + child_items = dfs(tree, item) + if child_items: + bom_item_code = prefix + item + bom = frappe.get_doc(doctype="BOM", item=bom_item_code) + for child_item in child_items.keys(): + bom.append("items", {"item_code": prefix + child_item}) + bom.insert() + bom.submit() + + return bom # parent bom is last bom + + def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): - if warehouse_list and isinstance(warehouse_list, string_types): + if warehouse_list and isinstance(warehouse_list, str): warehouse_list = [warehouse_list] if not warehouse_list: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 302753214b..180815d80e 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -468,46 +468,47 @@ class WorkOrder(Document): def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" - self.set('operations', []) + def _get_operations(bom_no, qty=1): + return frappe.db.sql( + f"""select + operation, description, workstation, idx, + base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, + "Pending" as status, parent as bom, batch_size, sequence_id + from + `tabBOM Operation` + where + parent = %s order by idx + """, bom_no, as_dict=1) + + + self.set('operations', []) if not self.bom_no: return - if self.use_multi_level_bom: - bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() + operations = [] + if not self.use_multi_level_bom: + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) else: - bom_list = [self.bom_no] + bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() + bom_traversal = list(reversed(bom_tree.level_order_traversal())) + bom_traversal.append(bom_tree) # add operation on top level item last + + for d in bom_traversal: + if d.is_bom: + operations.extend(_get_operations(d.name, qty=d.exploded_qty)) + + for correct_index, operation in enumerate(operations, start=1): + operation.idx = correct_index - operations = frappe.db.sql(""" - select - operation, description, workstation, idx, - base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id - from - `tabBOM Operation` - where - parent in (%s) order by idx - """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) self.set('operations', operations) - - if self.use_multi_level_bom and self.get('operations') and self.get('items'): - raw_material_operations = [d.operation for d in self.get('items')] - operations = [d.operation for d in self.get('operations')] - - for operation in raw_material_operations: - if operation not in operations: - self.append('operations', { - 'operation': operation - }) - self.calculate_time() def calculate_time(self): - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() From ca2dbcec593ba3d1845d038721790420db114189 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sat, 26 Jun 2021 01:32:17 +0530 Subject: [PATCH 258/550] fix(Asset Repair): Rearrange fields --- erpnext/assets/doctype/asset_repair/asset_repair.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 0651deb2ac..ba3189887c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -8,10 +8,10 @@ "engine": "InnoDB", "field_order": [ "asset", - "naming_series", + "company", "column_break_2", "asset_name", - "company", + "naming_series", "section_break_5", "failure_date", "repair_status", @@ -264,7 +264,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-21 14:53:46.665576", + "modified": "2021-06-25 13:14:38.307723", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", From e5c47f8957a0439b303684f49a963c308e2fad0e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Jun 2021 23:13:54 +0530 Subject: [PATCH 259/550] fix: fetch batch items in stock reco --- .../doctype/work_order/test_work_order.py | 9 +- .../stock_reconciliation.js | 67 +++++++---- .../stock_reconciliation.py | 108 +++++++++++++----- 3 files changed, 125 insertions(+), 59 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index cb1ee92196..68de0b29d3 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase): ste.submit() stock_entries.append(ste) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') self.assertEqual(len(job_cards), len(bom.operations)) for i, job_card in enumerate(job_cards): doc = frappe.get_doc("Job Card", job_card) - doc.append("time_logs", { - "from_time": add_to_date(None, i), - "hours": 1, - "to_time": add_to_date(None, i + 1), - "completed_qty": doc.for_quantity - }) + doc.time_logs[0].completed_qty = 1 doc.submit() ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index ac4ed5e75d..a01db80da4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -48,37 +48,54 @@ frappe.ui.form.on("Stock Reconciliation", { }, get_items: function(frm) { - frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1, + let fields = [{ + label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1, "get_query": function() { return { "filters": { "company": frm.doc.company, } - } - }}, - function(data) { - frappe.call({ - method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", - args: { - warehouse: data.warehouse, - posting_date: frm.doc.posting_date, - posting_time: frm.doc.posting_time, - company:frm.doc.company - }, - callback: function(r) { - var items = []; - frm.clear_table("items"); - for(var i=0; i< r.message.length; i++) { - var d = frm.add_child("items"); - $.extend(d, r.message[i]); - if(!d.qty) d.qty = null; - if(!d.valuation_rate) d.valuation_rate = null; - } - frm.refresh_field("items"); - } - }); + }; } - , __("Get Items"), __("Update")); + }, { + label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item", + "get_query": function() { + return { + "filters": { + "disabled": 0, + } + }; + } + }]; + + frappe.prompt(fields, function(data) { + frappe.call({ + method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", + args: { + warehouse: data.warehouse, + posting_date: frm.doc.posting_date, + posting_time: frm.doc.posting_time, + company: frm.doc.company, + item_code: data.item_code + }, + callback: function(r) { + frm.clear_table("items"); + for (var i=0; i= %s and rgt <= %s and name=bin.warehouse) - """, (lft, rgt)) + where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1 + and i.has_variants = 0 and exists( + select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse + ) + """, (lft, rgt), as_dict=1) items += frappe.db.sql(""" - select i.name, i.item_name, id.default_warehouse, i.has_serial_no + select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no from tabItem i, `tabItem Default` id where i.name = id.parent and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) - and i.is_stock_item = 1 and i.has_batch_no = 0 - and i.has_variants = 0 and i.disabled = 0 and id.company=%s + and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s group by i.name - """, (lft, rgt, company)) + """, (lft, rgt, company), as_dict=1) - res = [] - for d in set(items): - stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time, - with_valuation_rate=True , with_serial_no=cint(d[3])) + return items - if frappe.db.get_value("Item", d[0], "disabled") == 0: - res.append({ - "item_code": d[0], - "warehouse": d[2], - "qty": stock_bal[0], - "item_name": d[1], - "valuation_rate": stock_bal[1], - "current_qty": stock_bal[0], - "current_valuation_rate": stock_bal[1], - "current_serial_no": stock_bal[2] if cint(d[3]) else '', - "serial_no": stock_bal[2] if cint(d[3]) else '' - }) +def get_item_data(row, qty, valuation_rate, serial_no=None): + return { + 'item_code': row.item_code, + 'warehouse': row.warehouse, + 'qty': qty, + 'item_name': row.item_name, + 'valuation_rate': valuation_rate, + 'current_qty': qty, + 'current_valuation_rate': valuation_rate, + 'current_serial_no': serial_no, + 'serial_no': serial_no, + 'batch_no': row.get('batch_no') + } - return res +def get_itemwise_batch(warehouse, posting_date, company, item_code=None): + from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute + itemwise_batch_data = {} + + filters = frappe._dict({ + 'warehouse': warehouse, + 'from_date': posting_date, + 'to_date': posting_date, + 'company': company + }) + + if item_code: + filters.item_code = item_code + + columns, data = execute(filters) + + for row in data: + itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({ + 'item_code': row[0], + 'warehouse': warehouse, + 'qty': row[8], + 'item_name': row[1], + 'batch_no': row[4] + })) + + return itemwise_batch_data @frappe.whitelist() def get_stock_balance_for(item_code, warehouse, From 170f2ad05619f8bf92d3ecf5acaad4ca7c7cf356 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 22 Jun 2021 15:23:04 +0530 Subject: [PATCH 260/550] fix: removed values out of sync validation from stock transactions --- erpnext/controllers/stock_controller.py | 5 +- .../incorrect_stock_value_report/__init__.py | 0 .../incorrect_stock_value_report.js | 36 +++++ .../incorrect_stock_value_report.json | 29 ++++ .../incorrect_stock_value_report.py | 141 ++++++++++++++++++ 5 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 erpnext/stock/report/incorrect_stock_value_report/__init__.py create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 35097b97b9..8196cff849 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map -from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map from erpnext.stock.stock_ledger import get_valuation_rate @@ -497,9 +497,6 @@ class StockController(AccountsController): }) if future_sle_exists(args): create_repost_item_valuation_entry(args) - elif not is_reposting_pending(): - check_if_stock_and_account_balance_synced(self.posting_date, - self.company, self.doctype, self.name) @frappe.whitelist() def make_quality_inspections(doctype, docname, items): diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js new file mode 100644 index 0000000000..ff424807e3 --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js @@ -0,0 +1,36 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Stock Value Report"] = { + "filters": [ + { + "label": __("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "label": __("Account"), + "fieldname": "account", + "fieldtype": "Link", + "options": "Account", + get_query: function() { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "account_type": "Stock", + "company": company + } + } + } + }, + { + "label": __("From Date"), + "fieldname": "from_date", + "fieldtype": "Date" + } + ] +}; diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json new file mode 100644 index 0000000000..a7e9f203f7 --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-06-22 15:35:05.148177", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-06-22 15:35:05.148177", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Stock Value Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Stock Value Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py new file mode 100644 index 0000000000..a7243878eb --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -0,0 +1,141 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import erpnext +from frappe import _ +from six import iteritems +from frappe.utils import add_days, today, getdate +from erpnext.stock.utils import get_stock_value_on +from erpnext.accounts.utils import get_stock_and_account_balance + +def execute(filters=None): + if not erpnext.is_perpetual_inventory_enabled(filters.company): + frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") + .format(filters.company)) + + data = get_data(filters) + columns = get_columns(filters) + + return columns, data + +def get_unsync_date(filters): + date = filters.from_date + if not date: + date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") + date = date[0][0] + + if not date: + return + + while getdate(date) < getdate(today()): + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date, + company=filters.company, account = filters.account) + + if abs(account_bal - stock_bal) > 0.1: + return date + + date = add_days(date, 1) + +def get_data(report_filters): + from_date = get_unsync_date(report_filters) + + if not from_date: + return [] + + result = [] + + voucher_wise_dict = {} + data = frappe.db.sql(''' + SELECT + name, posting_date, posting_time, voucher_type, voucher_no, + stock_value_difference, stock_value, warehouse, item_code + FROM + `tabStock Ledger Entry` + WHERE + posting_date + = %s and company = %s + and is_cancelled = 0 + ORDER BY timestamp(posting_date, posting_time) asc, creation asc + ''', (from_date, report_filters.company), as_dict=1) + + for d in data: + voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) + + closing_date = add_days(from_date, -1) + for key, stock_data in iteritems(voucher_wise_dict): + prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1]) + for data in stock_data: + expected_stock_value = prev_stock_value + data.stock_value_difference + if abs(data.stock_value - expected_stock_value) > 0.1: + data.difference_value = abs(data.stock_value - expected_stock_value) + data.expected_stock_value = expected_stock_value + result.append(data) + + return result + +def get_columns(filters): + return [ + { + "label": _("Stock Ledger ID"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Stock Ledger Entry", + "width": "80" + }, + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date" + }, + { + "label": _("Posting Time"), + "fieldname": "posting_time", + "fieldtype": "Time" + }, + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "width": "110" + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": "110" + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": "110" + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": "110" + }, + { + "label": _("Expected Stock Value"), + "fieldname": "expected_stock_value", + "fieldtype": "Currency", + "width": "150" + }, + { + "label": _("Stock Value"), + "fieldname": "stock_value", + "fieldtype": "Currency", + "width": "120" + }, + { + "label": _("Difference Value"), + "fieldname": "difference_value", + "fieldtype": "Currency", + "width": "150" + } + ] \ No newline at end of file From 5d5dc56f94a00cf501dc3df0839020216d521cfd Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 22 Jun 2021 15:23:04 +0530 Subject: [PATCH 261/550] fix: removed values out of sync validation from stock transactions --- erpnext/controllers/stock_controller.py | 5 +- .../incorrect_stock_value_report/__init__.py | 0 .../incorrect_stock_value_report.js | 36 +++++ .../incorrect_stock_value_report.json | 29 ++++ .../incorrect_stock_value_report.py | 141 ++++++++++++++++++ 5 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 erpnext/stock/report/incorrect_stock_value_report/__init__.py create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 35097b97b9..8196cff849 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map -from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map from erpnext.stock.stock_ledger import get_valuation_rate @@ -497,9 +497,6 @@ class StockController(AccountsController): }) if future_sle_exists(args): create_repost_item_valuation_entry(args) - elif not is_reposting_pending(): - check_if_stock_and_account_balance_synced(self.posting_date, - self.company, self.doctype, self.name) @frappe.whitelist() def make_quality_inspections(doctype, docname, items): diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js new file mode 100644 index 0000000000..ff424807e3 --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js @@ -0,0 +1,36 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Stock Value Report"] = { + "filters": [ + { + "label": __("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "label": __("Account"), + "fieldname": "account", + "fieldtype": "Link", + "options": "Account", + get_query: function() { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "account_type": "Stock", + "company": company + } + } + } + }, + { + "label": __("From Date"), + "fieldname": "from_date", + "fieldtype": "Date" + } + ] +}; diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json new file mode 100644 index 0000000000..a7e9f203f7 --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-06-22 15:35:05.148177", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-06-22 15:35:05.148177", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Stock Value Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Stock Value Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py new file mode 100644 index 0000000000..a7243878eb --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -0,0 +1,141 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import erpnext +from frappe import _ +from six import iteritems +from frappe.utils import add_days, today, getdate +from erpnext.stock.utils import get_stock_value_on +from erpnext.accounts.utils import get_stock_and_account_balance + +def execute(filters=None): + if not erpnext.is_perpetual_inventory_enabled(filters.company): + frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") + .format(filters.company)) + + data = get_data(filters) + columns = get_columns(filters) + + return columns, data + +def get_unsync_date(filters): + date = filters.from_date + if not date: + date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") + date = date[0][0] + + if not date: + return + + while getdate(date) < getdate(today()): + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date, + company=filters.company, account = filters.account) + + if abs(account_bal - stock_bal) > 0.1: + return date + + date = add_days(date, 1) + +def get_data(report_filters): + from_date = get_unsync_date(report_filters) + + if not from_date: + return [] + + result = [] + + voucher_wise_dict = {} + data = frappe.db.sql(''' + SELECT + name, posting_date, posting_time, voucher_type, voucher_no, + stock_value_difference, stock_value, warehouse, item_code + FROM + `tabStock Ledger Entry` + WHERE + posting_date + = %s and company = %s + and is_cancelled = 0 + ORDER BY timestamp(posting_date, posting_time) asc, creation asc + ''', (from_date, report_filters.company), as_dict=1) + + for d in data: + voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) + + closing_date = add_days(from_date, -1) + for key, stock_data in iteritems(voucher_wise_dict): + prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1]) + for data in stock_data: + expected_stock_value = prev_stock_value + data.stock_value_difference + if abs(data.stock_value - expected_stock_value) > 0.1: + data.difference_value = abs(data.stock_value - expected_stock_value) + data.expected_stock_value = expected_stock_value + result.append(data) + + return result + +def get_columns(filters): + return [ + { + "label": _("Stock Ledger ID"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Stock Ledger Entry", + "width": "80" + }, + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date" + }, + { + "label": _("Posting Time"), + "fieldname": "posting_time", + "fieldtype": "Time" + }, + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "width": "110" + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": "110" + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": "110" + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": "110" + }, + { + "label": _("Expected Stock Value"), + "fieldname": "expected_stock_value", + "fieldtype": "Currency", + "width": "150" + }, + { + "label": _("Stock Value"), + "fieldname": "stock_value", + "fieldtype": "Currency", + "width": "120" + }, + { + "label": _("Difference Value"), + "fieldname": "difference_value", + "fieldtype": "Currency", + "width": "150" + } + ] \ No newline at end of file From 478360397d903bcb374d5cb7fd862337cedc59ea Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Jun 2021 23:13:54 +0530 Subject: [PATCH 262/550] fix: fetch batch items in stock reco --- .../doctype/work_order/test_work_order.py | 9 +- .../stock_reconciliation.js | 67 +++++++---- .../stock_reconciliation.py | 108 +++++++++++++----- 3 files changed, 125 insertions(+), 59 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index cb1ee92196..68de0b29d3 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase): ste.submit() stock_entries.append(ste) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') self.assertEqual(len(job_cards), len(bom.operations)) for i, job_card in enumerate(job_cards): doc = frappe.get_doc("Job Card", job_card) - doc.append("time_logs", { - "from_time": add_to_date(None, i), - "hours": 1, - "to_time": add_to_date(None, i + 1), - "completed_qty": doc.for_quantity - }) + doc.time_logs[0].completed_qty = 1 doc.submit() ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index ac4ed5e75d..a01db80da4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -48,37 +48,54 @@ frappe.ui.form.on("Stock Reconciliation", { }, get_items: function(frm) { - frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1, + let fields = [{ + label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1, "get_query": function() { return { "filters": { "company": frm.doc.company, } - } - }}, - function(data) { - frappe.call({ - method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", - args: { - warehouse: data.warehouse, - posting_date: frm.doc.posting_date, - posting_time: frm.doc.posting_time, - company:frm.doc.company - }, - callback: function(r) { - var items = []; - frm.clear_table("items"); - for(var i=0; i< r.message.length; i++) { - var d = frm.add_child("items"); - $.extend(d, r.message[i]); - if(!d.qty) d.qty = null; - if(!d.valuation_rate) d.valuation_rate = null; - } - frm.refresh_field("items"); - } - }); + }; } - , __("Get Items"), __("Update")); + }, { + label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item", + "get_query": function() { + return { + "filters": { + "disabled": 0, + } + }; + } + }]; + + frappe.prompt(fields, function(data) { + frappe.call({ + method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", + args: { + warehouse: data.warehouse, + posting_date: frm.doc.posting_date, + posting_time: frm.doc.posting_time, + company: frm.doc.company, + item_code: data.item_code + }, + callback: function(r) { + frm.clear_table("items"); + for (var i=0; i= %s and rgt <= %s and name=bin.warehouse) - """, (lft, rgt)) + where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1 + and i.has_variants = 0 and exists( + select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse + ) + """, (lft, rgt), as_dict=1) items += frappe.db.sql(""" - select i.name, i.item_name, id.default_warehouse, i.has_serial_no + select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no from tabItem i, `tabItem Default` id where i.name = id.parent and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) - and i.is_stock_item = 1 and i.has_batch_no = 0 - and i.has_variants = 0 and i.disabled = 0 and id.company=%s + and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s group by i.name - """, (lft, rgt, company)) + """, (lft, rgt, company), as_dict=1) - res = [] - for d in set(items): - stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time, - with_valuation_rate=True , with_serial_no=cint(d[3])) + return items - if frappe.db.get_value("Item", d[0], "disabled") == 0: - res.append({ - "item_code": d[0], - "warehouse": d[2], - "qty": stock_bal[0], - "item_name": d[1], - "valuation_rate": stock_bal[1], - "current_qty": stock_bal[0], - "current_valuation_rate": stock_bal[1], - "current_serial_no": stock_bal[2] if cint(d[3]) else '', - "serial_no": stock_bal[2] if cint(d[3]) else '' - }) +def get_item_data(row, qty, valuation_rate, serial_no=None): + return { + 'item_code': row.item_code, + 'warehouse': row.warehouse, + 'qty': qty, + 'item_name': row.item_name, + 'valuation_rate': valuation_rate, + 'current_qty': qty, + 'current_valuation_rate': valuation_rate, + 'current_serial_no': serial_no, + 'serial_no': serial_no, + 'batch_no': row.get('batch_no') + } - return res +def get_itemwise_batch(warehouse, posting_date, company, item_code=None): + from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute + itemwise_batch_data = {} + + filters = frappe._dict({ + 'warehouse': warehouse, + 'from_date': posting_date, + 'to_date': posting_date, + 'company': company + }) + + if item_code: + filters.item_code = item_code + + columns, data = execute(filters) + + for row in data: + itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({ + 'item_code': row[0], + 'warehouse': warehouse, + 'qty': row[8], + 'item_name': row[1], + 'batch_no': row[4] + })) + + return itemwise_batch_data @frappe.whitelist() def get_stock_balance_for(item_code, warehouse, From 1e5482cedd605f2398f6dbeff4efe40a8cbc1848 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 26 Jun 2021 23:49:32 +0530 Subject: [PATCH 263/550] fix: Revert Changes --- erpnext/public/js/setup_wizard.js | 26 +++++++ .../setup_wizard/data/country_wise_tax.json | 72 ++++++++++++------- .../setup_wizard/operations/taxes_setup.py | 15 ++-- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index a3045724fe..6f5d67c746 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -139,10 +139,36 @@ erpnext.setup.slides_settings = [ }, validate: function () { + let me = this; + let exist; + if (!this.validate_fy_dates()) { return false; } + // Validate bank name + if(me.values.bank_account) { + frappe.call({ + async: false, + method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", + args: { + "coa": me.values.chart_of_accounts, + "bank_account": me.values.bank_account + }, + callback: function (r) { + if(r.message){ + exist = r.message; + me.get_field("bank_account").set_value(""); + let message = __('Account {0} already exists. Please enter a different name for your bank account.', + [me.values.bank_account] + ); + frappe.msgprint(message); + } + } + }); + return !exist; // Return False if exist = true + } + return true; }, diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index e36bf5cbe0..34af093a23 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1218,37 +1218,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 18.00 + "tax_rate": 18.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 18.00 + "tax_rate": 18.00, + "root_type": "Asset" } } ] @@ -1277,37 +1283,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 2.5 + "tax_rate": 2.5, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 2.5 + "tax_rate": 2.5, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 5.0 + "tax_rate": 5.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 2.50 + "tax_rate": 2.50, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 2.50 + "tax_rate": 2.50, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 5.00 + "tax_rate": 5.00, + "root_type": "Asset" } } ] @@ -1336,37 +1348,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 6.0 + "tax_rate": 6.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 6.0 + "tax_rate": 6.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 12.0 + "tax_rate": 12.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 6.00 + "tax_rate": 6.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 6.00 + "tax_rate": 6.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 12.00 + "tax_rate": 12.00, + "root_type": "Asset" } } ] @@ -1395,37 +1413,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 14.0 + "tax_rate": 14.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 14.0 + "tax_rate": 14.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 28.0 + "tax_rate": 28.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 14.00 + "tax_rate": 14.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 14.00 + "tax_rate": 14.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 28.00 + "tax_rate": 28.00, + "root_type": "Asset" } } ] diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index d5682b6f4c..9f73f214bd 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -203,16 +203,15 @@ def get_or_create_account(company_name, account): existing_accounts = frappe.get_list('Account', filters={ - 'account_name': account.get('account_name'), - 'account_number': account.get('account_number', ''), - 'company': company_name + 'company': company_name, + 'root_type': root_type }, or_filters={ - 'company': company_name, - 'root_type': root_type, - 'is_group': 0 - } - ) + 'account_name': account.get('account_name'), + 'account_number': account.get('account_number') + }) + + print(company_name, account, existing_accounts) if existing_accounts: return frappe.get_doc('Account', existing_accounts[0].name) From cf445eb7b4f057dfb57c0861745051d5e65dccb9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 26 Jun 2021 23:59:47 +0530 Subject: [PATCH 264/550] fix: Add validate bank account method back --- .../chart_of_accounts/chart_of_accounts.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 9b6842d896..927adc7086 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -188,6 +188,24 @@ def build_account_tree(tree, parent, all_accounts): # call recursively to build a subtree for current account build_account_tree(tree[child.account_name], child, all_accounts) +@frappe.whitelist() +def validate_bank_account(coa, bank_account): + accounts = [] + chart = get_chart(coa) + + if chart: + def _get_account_names(account_master): + for account_name, child in iteritems(account_master): + if account_name not in ["account_number", "account_type", + "root_type", "is_group", "tax_rate"]: + accounts.append(account_name) + + _get_account_names(child) + + _get_account_names(chart) + + return (bank_account in accounts) + @frappe.whitelist() def build_tree_from_json(chart_template, chart_data=None): ''' get chart template from its folder and parse the json to be rendered as tree ''' From ebc46c1e09b26fc6a26ad386272affd78fa2948e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Jun 2021 10:52:38 +0530 Subject: [PATCH 265/550] fix: Update account heads in GST test cases --- .../sales_invoice/test_sales_invoice.py | 63 ++++++++++--------- .../gstr_3b_report/test_gstr_3b_report.py | 28 ++++----- .../setup_wizard/operations/taxes_setup.py | 2 - 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 114b7d2d35..dbc7f8632f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1957,6 +1957,33 @@ class TestSalesInvoice(unittest.TestCase): einvoice = make_einvoice(si) validate_totals(einvoice) + def test_item_tax_net_range(self): + item = create_item("T Shirt") + + item.set('taxes', []) + item.append("taxes", { + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + "minimum_net_rate": 0, + "maximum_net_rate": 500 + }) + + item.append("taxes", { + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + "minimum_net_rate": 501, + "maximum_net_rate": 1000 + }) + + item.save() + + sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True) + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") + + # Apply discount + sales_invoice.apply_discount_on = 'Net Total' + sales_invoice.discount_amount = 300 + sales_invoice.save() + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' @@ -1985,32 +2012,6 @@ def get_sales_invoice_for_e_invoice(): return si - def test_item_tax_net_range(self): - item = create_item("T Shirt") - - item.set('taxes', []) - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", - "minimum_net_rate": 0, - "maximum_net_rate": 500 - }) - - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", - "minimum_net_rate": 501, - "maximum_net_rate": 1000 - }) - - item.save() - - sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True) - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") - - # Apply discount - sales_invoice.apply_discount_on = 'Net Total' - sales_invoice.discount_amount = 300 - sales_invoice.save() - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") def make_test_address_for_ewaybill(): if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): @@ -2087,9 +2088,9 @@ def make_sales_invoice_for_ewaybill(): if not gst_account: gst_settings.append("gst_accounts", { "company": "_Test Company", - "cgst_account": "CGST - _TC", - "sgst_account": "SGST - _TC", - "igst_account": "IGST - _TC", + "cgst_account": "Output Tax CGST - _TC", + "sgst_account": "Output Tax SGST - _TC", + "igst_account": "Output Tax IGST - _TC", }) gst_settings.save() @@ -2106,7 +2107,7 @@ def make_sales_invoice_for_ewaybill(): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "CGST - _TC", + "account_head": "Output Tax CGST - _TC", "cost_center": "Main - _TC", "description": "CGST @ 9.0", "rate": 9 @@ -2114,7 +2115,7 @@ def make_sales_invoice_for_ewaybill(): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "SGST - _TC", + "account_head": "Output Tax SGST - _TC", "cost_center": "Main - _TC", "description": "SGST @ 9.0", "rate": 9 diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index 3857ce1cdb..065f80d610 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -46,14 +46,14 @@ class TestGSTR3BReport(unittest.TestCase): make_sales_invoice() create_purchase_invoices() - if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing"): - report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing") + if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing"): + report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing") report.save() else: report = frappe.get_doc({ "doctype": "GSTR 3B Report", "company": "_Test Company GST", - "company_address": "_Test Address-Billing", + "company_address": "_Test Address GST-Billing", "year": getdate().year, "month": month_number_mapping.get(getdate().month) }).insert() @@ -89,7 +89,7 @@ class TestGSTR3BReport(unittest.TestCase): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -117,7 +117,7 @@ def make_sales_invoice(): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -138,7 +138,7 @@ def make_sales_invoice(): si1.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -159,7 +159,7 @@ def make_sales_invoice(): si2.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -195,7 +195,7 @@ def create_purchase_invoices(): pi.append("taxes", { "charge_type": "On Net Total", - "account_head": "CGST - _GST", + "account_head": "Input Tax CGST - _GST", "cost_center": "Main - _GST", "description": "CGST @ 9.0", "rate": 9 @@ -203,7 +203,7 @@ def create_purchase_invoices(): pi.append("taxes", { "charge_type": "On Net Total", - "account_head": "SGST - _GST", + "account_head": "Input Tax SGST - _GST", "cost_center": "Main - _GST", "description": "SGST @ 9.0", "rate": 9 @@ -410,10 +410,10 @@ def make_company(): company.country = "India" company.insert() - if not frappe.db.exists('Address', '_Test Address-Billing'): + if not frappe.db.exists('Address', '_Test Address GST-Billing'): address = frappe.get_doc({ + "address_title": "_Test Address GST", "address_line1": "_Test Address Line 1", - "address_title": "_Test Address", "address_type": "Billing", "city": "_Test City", "state": "Test State", @@ -444,9 +444,9 @@ def set_account_heads(): if not gst_account: gst_settings.append("gst_accounts", { "company": "_Test Company GST", - "cgst_account": "CGST - _GST", - "sgst_account": "SGST - _GST", - "igst_account": "IGST - _GST", + "cgst_account": "Output Tax CGST - _GST", + "sgst_account": "Output Tax SGST - _GST", + "igst_account": "Output Tax IGST - _GST" }) gst_settings.save() diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 9f73f214bd..c7cc000518 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -211,8 +211,6 @@ def get_or_create_account(company_name, account): 'account_number': account.get('account_number') }) - print(company_name, account, existing_accounts) - if existing_accounts: return frappe.get_doc('Account', existing_accounts[0].name) From 7d7d797ffc80956e13493d98d92097cc8d280863 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Jun 2021 11:24:32 +0530 Subject: [PATCH 266/550] fix: Do not consider cancelled entries in party dashboard --- erpnext/accounts/party.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e025fc6905..b97dc401e6 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where party_type = %s and party=%s + and is_cancelled = 0 group by company""", (party_type, party))) for d in companies: From 40c90d03a4e583ff374e180b3abbf95b31f74fed Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 28 Jun 2021 11:42:28 +0530 Subject: [PATCH 267/550] fix(Asset Repair): cancellation --- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 64c51fd8c3..d32fdf7054 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -30,7 +30,7 @@ class AssetRepair(AccountsController): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) def calculate_total_repair_cost(self): - self.total_repair_cost = self.repair_cost + self.total_repair_cost = flt(self.repair_cost) total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() self.total_repair_cost += total_value_of_stock_consumed @@ -129,6 +129,7 @@ class AssetRepair(AccountsController): def increase_stock_quantity(self): stock_entry = frappe.get_doc('Stock Entry', self.stock_entry) + stock_entry.flags.ignore_links = True stock_entry.cancel() def make_gl_entries(self, cancel=False): @@ -252,4 +253,4 @@ class AssetRepair(AccountsController): @frappe.whitelist() def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) - return round(downtime, 2) \ No newline at end of file + return round(downtime, 2) From 785a71dcd2444cc085f286140c7595e95ad47d09 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Sun, 6 Jun 2021 22:01:18 +0530 Subject: [PATCH 268/550] fix(Purchase Invoice): Resolve difference caused by change in exchange rate --- .../purchase_invoice/purchase_invoice.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 45d89ad1c8..85b4642604 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -634,6 +634,29 @@ class PurchaseInvoice(BuyingController): "project": item.project or self.project }, account_currency, item=item)) + # check if the exchange rate has changed + purchase_receipt_conversion_rate = frappe.db.get_value('Purchase Receipt', {'name': item.purchase_receipt}, ['conversion_rate']) + if self.conversion_rate != purchase_receipt_conversion_rate: + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.rate) * (purchase_receipt_conversion_rate - self.conversion_rate) + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "against": self.supplier, + "debit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + gl_entries.append( + self.get_gl_dict({ + "account": self.get_company_default("exchange_gain_loss_account"), + "against": self.supplier, + "credit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") From 6233eaa3bb6d31df7c47dc0d0be15d52633ddf4a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 05:28:20 +0530 Subject: [PATCH 269/550] fix(Purchase Invoice): Fix condition --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 85b4642604..01df8060ac 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -636,7 +636,7 @@ class PurchaseInvoice(BuyingController): # check if the exchange rate has changed purchase_receipt_conversion_rate = frappe.db.get_value('Purchase Receipt', {'name': item.purchase_receipt}, ['conversion_rate']) - if self.conversion_rate != purchase_receipt_conversion_rate: + if purchase_receipt_conversion_rate and self.conversion_rate != purchase_receipt_conversion_rate: discrepancy_caused_by_exchange_rate_difference = (item.qty * item.rate) * (purchase_receipt_conversion_rate - self.conversion_rate) gl_entries.append( self.get_gl_dict({ From 206313d69bfc591afd59ee56b656717543bfd1c1 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 06:11:57 +0530 Subject: [PATCH 270/550] fix(Purchase Invoice): Add test for exchange rate difference handling --- .../purchase_invoice/test_purchase_invoice.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2f5d36c8fa..7f350e7ed5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -230,6 +230,23 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) + def test_purchase_invoice_with_exchange_rate_difference(self): + set_gst_settings() + pr = make_purchase_receipt(currency = "USD", conversion_rate = 70) + pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True") + + for item in pi.items: + item.purchase_receipt = pr.name + + pi.insert() + pi.submit() + + # fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account + gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'}) + voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') + + self.assertEqual(pi.name, voucher_no) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() @@ -1050,6 +1067,24 @@ def update_tax_witholding_category(company, account, date): 'account': account }) tds_category.save() +def set_gst_settings(): + gst_settings = frappe.get_doc("GST Settings") + + gst_account = frappe.get_all( + "GST Account", + fields=["cgst_account", "sgst_account", "igst_account"], + filters = {"company": "_Test Company"} + ) + + if not gst_account: + gst_settings.append("gst_accounts", { + "company": "_Test Company", + "cgst_account": "CGST - _TC", + "sgst_account": "SGST - _TC", + "igst_account": "IGST - _TC", + }) + + gst_settings.save() def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From d97505b277535760b2cf3f2352ea10660dcefd46 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 07:09:11 +0530 Subject: [PATCH 271/550] fix(Purchase Receipt): Resolve difference caused by change in exchange rate --- .../purchase_receipt/purchase_receipt.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e488b695b5..54cd33242b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -288,6 +288,10 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, stock_rbnb, account_currency=warehouse_account_currency, item=d) + print("*"* 30) + print(1) + print("warehouse_account_name: ", warehouse_account_name) + print("") # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation @@ -303,6 +307,19 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, account, d.cost_center, -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name, debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d) + + # check if the exchange rate has changed + purchase_invoice_conversion_rate = frappe.db.get_value('Purchase Invoice', {'name': d.purchase_invoice}, ['conversion_rate']) + if purchase_invoice_conversion_rate and self.conversion_rate != purchase_invoice_conversion_rate: + discrepancy_caused_by_exchange_rate_difference = (d.qty * d.rate) * (purchase_invoice_conversion_rate - self.conversion_rate) + + self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) + + self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: From 5314445d58f36e7abb527911e3446ebf909edcc6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 7 Jun 2021 08:02:11 +0530 Subject: [PATCH 272/550] fix(Purchase Receipt): Add test for exchange rate difference handling --- .../purchase_receipt/test_purchase_receipt.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2586a0fc0c..0ce4c3a669 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1051,6 +1051,28 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(len(item_two_gl_entry), 1) frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def test_purchase_receipt_with_exchange_rate_difference(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice + + pi = create_purchase_invoice(currency = "USD", conversion_rate = 70) + + create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", + properties={"account": '_Test Account Stock In Hand - TCP1'}) + pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', + company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80, + do_not_save = "True") + + for item in pr.items: + item.purchase_invoice = pi.name + + pr.insert() + pr.submit() + + # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account + gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'}) + voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') + + self.assertEqual(pr.name, voucher_no) def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference From feed97ec53767e777b8a638d5a8e3ffc0a04e500 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 11 Jun 2021 17:27:43 +0530 Subject: [PATCH 273/550] fix: Test --- ...tracted_raw_materials_to_be_transferred.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 2448e17c50..f1784925c5 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -60,6 +60,7 @@ def transfer_subcontracted_raw_materials(po): rm_item = [ { 'name': po.supplied_items[0].name, +<<<<<<< HEAD 'item_code': item_1, 'rm_item_code': item_1, 'item_name': item_1, @@ -67,10 +68,20 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_1], +======= + 'item_code': '_Test Item Home Desktop 100', + 'rm_item_code': '_Test Item Home Desktop 100', + 'item_name': '_Test Item Home Desktop 100', + 'qty': 2, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 200, +>>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' }, { 'name': po.supplied_items[1].name, +<<<<<<< HEAD 'item_code': item_2, 'rm_item_code': item_2, 'item_name': item_2, @@ -78,6 +89,15 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_2], +======= + 'item_code': '_Test Item', + 'rm_item_code': '_Test Item', + 'item_name': '_Test Item', + 'qty': 1, + 'warehouse': '_Test Warehouse - _TC', + 'rate': 100, + 'amount': 100, +>>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' } ] From 3f5e7bf6d703807f6774d86839f39f6587b1f576 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 06:13:38 +0530 Subject: [PATCH 274/550] fix(Purchase Invoice): Fetch Purchase Receipt details using a function --- .../purchase_invoice/purchase_invoice.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 01df8060ac..32e2eb82f8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,6 +517,8 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] + purchase_receipt_details = self.get_purchase_receipt_details() + for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) @@ -634,10 +636,13 @@ class PurchaseInvoice(BuyingController): "project": item.project or self.project }, account_currency, item=item)) - # check if the exchange rate has changed - purchase_receipt_conversion_rate = frappe.db.get_value('Purchase Receipt', {'name': item.purchase_receipt}, ['conversion_rate']) - if purchase_receipt_conversion_rate and self.conversion_rate != purchase_receipt_conversion_rate: - discrepancy_caused_by_exchange_rate_difference = (item.qty * item.rate) * (purchase_receipt_conversion_rate - self.conversion_rate) + if purchase_receipt_details[item.item_code]["conversion_rate"] and \ + self.conversion_rate != purchase_receipt_details[item.item_code]["conversion_rate"] and \ + item.net_rate == purchase_receipt_details[item.item_code]["net_rate"]: + + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ + (purchase_receipt_details[item.item_code]["conversion_rate"] - self.conversion_rate) + gl_entries.append( self.get_gl_dict({ "account": expense_account, @@ -710,6 +715,23 @@ class PurchaseInvoice(BuyingController): self.negative_expense_to_be_booked += flt(item.item_tax_amount, \ item.precision("item_tax_amount")) + def get_purchase_receipt_details(self): + purchase_receipt_details = {} + for item in self.items: + if item.purchase_receipt: + purchase_receipt = frappe.get_doc('Purchase Receipt', item.purchase_receipt) + pr_item_details = { + "conversion_rate" : purchase_receipt.conversion_rate + } + + for pr_item in purchase_receipt.items: + if pr_item.item_code == item.item_code: + pr_item_details["net_rate"] = pr_item.net_rate + + purchase_receipt_details[item.item_code] = pr_item_details + + return purchase_receipt_details + def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") From a826fb8f95144a9e14d961bbe6d779c4c5aa668c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 06:18:35 +0530 Subject: [PATCH 275/550] fix(Purchase Receipt): Remove print statements --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 54cd33242b..305eb02714 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -287,11 +287,7 @@ class PurchaseReceipt(BuyingController): continue self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, - stock_rbnb, account_currency=warehouse_account_currency, item=d) - print("*"* 30) - print(1) - print("warehouse_account_name: ", warehouse_account_name) - print("") + stock_rbnb, account_currency=warehouse_account_currency, item=d) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation From 8f52db788a898262c8b546cc42a31e86f1a43560 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 16 Jun 2021 06:38:39 +0530 Subject: [PATCH 276/550] fix(Purchase Invoice): Improve test for exchange rate difference handling --- .../doctype/purchase_invoice/test_purchase_invoice.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7f350e7ed5..c38b9017c8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -247,6 +247,11 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(pi.name, voucher_no) + exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) + + self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() From 506d5ac3d58e6266037f16e9fab4dec506171334 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 28 Jun 2021 15:08:28 +0530 Subject: [PATCH 277/550] fix: Make exchange rate handling more efficient --- .../purchase_invoice/purchase_invoice.py | 77 ++++++++++--------- .../purchase_receipt/purchase_receipt.py | 48 +++++++++--- 2 files changed, 78 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 32e2eb82f8..05d632219a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - purchase_receipt_details = self.get_purchase_receipt_details() + exchange_rate_map, net_rate_map = self.get_purchase_receipt_details() for item in self.get("items"): if flt(item.base_net_amount): @@ -636,31 +636,33 @@ class PurchaseInvoice(BuyingController): "project": item.project or self.project }, account_currency, item=item)) - if purchase_receipt_details[item.item_code]["conversion_rate"] and \ - self.conversion_rate != purchase_receipt_details[item.item_code]["conversion_rate"] and \ - item.net_rate == purchase_receipt_details[item.item_code]["net_rate"]: + # check if the exchange rate has changed + if item.get('purchase_receipt'): + if exchange_rate_map[item.purchase_receipt] and \ + self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \ + item.net_rate == net_rate_map[item.item_code]: - discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ - (purchase_receipt_details[item.item_code]["conversion_rate"] - self.conversion_rate) + discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ + (exchange_rate_map[item.purchase_receipt] - self.conversion_rate) - gl_entries.append( - self.get_gl_dict({ - "account": expense_account, - "against": self.supplier, - "debit": discrepancy_caused_by_exchange_rate_difference, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) - gl_entries.append( - self.get_gl_dict({ - "account": self.get_company_default("exchange_gain_loss_account"), - "against": self.supplier, - "credit": discrepancy_caused_by_exchange_rate_difference, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) - ) + gl_entries.append( + self.get_gl_dict({ + "account": expense_account, + "against": self.supplier, + "debit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) + gl_entries.append( + self.get_gl_dict({ + "account": self.get_company_default("exchange_gain_loss_account"), + "against": self.supplier, + "credit": discrepancy_caused_by_exchange_rate_difference, + "cost_center": item.cost_center, + "project": item.project or self.project + }, account_currency, item=item) + ) # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: @@ -716,21 +718,22 @@ class PurchaseInvoice(BuyingController): item.precision("item_tax_amount")) def get_purchase_receipt_details(self): - purchase_receipt_details = {} - for item in self.items: - if item.purchase_receipt: - purchase_receipt = frappe.get_doc('Purchase Receipt', item.purchase_receipt) - pr_item_details = { - "conversion_rate" : purchase_receipt.conversion_rate - } - - for pr_item in purchase_receipt.items: - if pr_item.item_code == item.item_code: - pr_item_details["net_rate"] = pr_item.net_rate + purchase_receipts = [] + pr_items = [] - purchase_receipt_details[item.item_code] = pr_item_details + for item in self.get('items'): + if item.get('purchase_receipt'): + purchase_receipts.append(item.purchase_receipt) + if item.get('pr_detail'): + pr_items.append(item.pr_detail) + + exchange_rate_map = frappe._dict(frappe.get_all('Purchase Receipt', filters={'name': ('in', + purchase_receipts)}, fields=['name', 'conversion_rate'], as_list=1)) - return purchase_receipt_details + net_rate_map = frappe._dict(frappe.get_all('Purchase Receipt Item', filters={'name': ('in', + pr_items)}, fields=['item_code', 'net_rate'], as_list=1)) + + return exchange_rate_map, net_rate_map def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 305eb02714..7b002b0fc6 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -262,6 +262,8 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() + exchange_rate_map, net_rate_map = self.get_purchase_invoice_details() + for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if warehouse_account.get(d.warehouse): @@ -303,19 +305,23 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, account, d.cost_center, -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name, debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d) - + # check if the exchange rate has changed - purchase_invoice_conversion_rate = frappe.db.get_value('Purchase Invoice', {'name': d.purchase_invoice}, ['conversion_rate']) - if purchase_invoice_conversion_rate and self.conversion_rate != purchase_invoice_conversion_rate: - discrepancy_caused_by_exchange_rate_difference = (d.qty * d.rate) * (purchase_invoice_conversion_rate - self.conversion_rate) + if d.get('purchase_invoice'): + if exchange_rate_map[d.purchase_invoice] and \ + self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \ + d.net_rate == net_rate_map[d.item_code]: - self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, item=d) + discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \ + (exchange_rate_map[d.purchase_invoice] - self.conversion_rate) - self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, - account_currency=credit_currency, item=d) + self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) + + self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + account_currency=credit_currency, item=d) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: @@ -495,6 +501,28 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), remarks, expenses_included_in_asset_valuation, project=item.project, item=item) + def get_purchase_invoice_details(self): + purchase_invoices = [] + pi_items = [] + + for item in self.get('items'): + if item.get('purchase_invoice'): + purchase_invoices.append(item.purchase_invoice) + if item.get('purchase_invoice_item'): + pi_items.append(item.purchase_invoice_item) + + exchange_rate_map = frappe._dict(frappe.get_all('Purchase Invoice', filters={'name': ('in', + purchase_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) + print("*"*50) + print("In get_purchase_invoice_details:") + print("exchange_rate_map: ", exchange_rate_map) + + net_rate_map = frappe._dict(frappe.get_all('Purchase Invoice Item', filters={'name': ('in', + pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) + print("net_rate_map: ", net_rate_map) + + return exchange_rate_map, net_rate_map + def update_assets(self, item, valuation_rate): assets = frappe.db.get_all('Asset', filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } From d748e7f49cbbdf46e6eabf4c6168ecb80cbe065a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 28 Jun 2021 15:33:15 +0530 Subject: [PATCH 278/550] fix: Create common function for fetching conversion rate details of linked docs --- .../purchase_invoice/purchase_invoice.py | 50 ++++++++++++------- .../purchase_receipt/purchase_receipt.py | 26 ++-------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 05d632219a..c164868678 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - exchange_rate_map, net_rate_map = self.get_purchase_receipt_details() + exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) for item in self.get("items"): if flt(item.base_net_amount): @@ -717,24 +717,6 @@ class PurchaseInvoice(BuyingController): self.negative_expense_to_be_booked += flt(item.item_tax_amount, \ item.precision("item_tax_amount")) - def get_purchase_receipt_details(self): - purchase_receipts = [] - pr_items = [] - - for item in self.get('items'): - if item.get('purchase_receipt'): - purchase_receipts.append(item.purchase_receipt) - if item.get('pr_detail'): - pr_items.append(item.pr_detail) - - exchange_rate_map = frappe._dict(frappe.get_all('Purchase Receipt', filters={'name': ('in', - purchase_receipts)}, fields=['name', 'conversion_rate'], as_list=1)) - - net_rate_map = frappe._dict(frappe.get_all('Purchase Receipt Item', filters={'name': ('in', - pr_items)}, fields=['item_code', 'net_rate'], as_list=1)) - - return exchange_rate_map, net_rate_map - def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") @@ -1189,6 +1171,36 @@ class PurchaseInvoice(BuyingController): if update: self.db_set('status', self.status, update_modified = update_modified) +# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling +def get_pr_or_pi_details(doc): + if doc.doctype == 'Purchase Invoice': + pr_or_pi = 'purchase_receipt' + items_reference = 'pr_detail' + pr_or_pi_doctype = 'Purchase Receipt' + pr_or_pi_items_table = 'Purchase Receipt Item' + else: + pr_or_pi = 'purchase_invoice' + items_reference = 'purchase_invoice_item' + pr_or_pi_doctype = 'Purchase Invoice' + pr_or_pi_items_table = 'Purchase Invoice Item' + + purchase_receipts_or_invoices = [] + pr_or_pi_items = [] + + for item in doc.get('items'): + if item.get(pr_or_pi): + purchase_receipts_or_invoices.append(item.get(pr_or_pi)) + if item.get(items_reference): + pr_or_pi_items.append(item.get(items_reference)) + + exchange_rate_map = frappe._dict(frappe.get_all(pr_or_pi_doctype, filters={'name': ('in', + purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) + + net_rate_map = frappe._dict(frappe.get_all(pr_or_pi_items_table, filters={'name': ('in', + pr_or_pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) + + return exchange_rate_map, net_rate_map + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context list_context = get_list_context(context) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 7b002b0fc6..12fa5302d5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -254,6 +254,8 @@ class PurchaseReceipt(BuyingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_pr_or_pi_details + stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") @@ -262,7 +264,7 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() - exchange_rate_map, net_rate_map = self.get_purchase_invoice_details() + exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): @@ -501,28 +503,6 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount), remarks, expenses_included_in_asset_valuation, project=item.project, item=item) - def get_purchase_invoice_details(self): - purchase_invoices = [] - pi_items = [] - - for item in self.get('items'): - if item.get('purchase_invoice'): - purchase_invoices.append(item.purchase_invoice) - if item.get('purchase_invoice_item'): - pi_items.append(item.purchase_invoice_item) - - exchange_rate_map = frappe._dict(frappe.get_all('Purchase Invoice', filters={'name': ('in', - purchase_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) - print("*"*50) - print("In get_purchase_invoice_details:") - print("exchange_rate_map: ", exchange_rate_map) - - net_rate_map = frappe._dict(frappe.get_all('Purchase Invoice Item', filters={'name': ('in', - pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) - print("net_rate_map: ", net_rate_map) - - return exchange_rate_map, net_rate_map - def update_assets(self, item, valuation_rate): assets = frappe.db.get_all('Asset', filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } From 2d1c4fee1d79e06e90effe283cdf8ab47c98d9de Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Jun 2021 00:31:14 +0530 Subject: [PATCH 279/550] fix: allow to changes to date in the blanket order --- .../manufacturing/doctype/blanket_order/blanket_order.js | 2 +- .../manufacturing/doctype/blanket_order/blanket_order.json | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index 4c31bd0b7d..f19a1b0868 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Blanket Order', { refresh: function(frm) { erpnext.hide_company(); - if (frm.doc.customer && frm.doc.docstatus === 1) { + if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) { frm.add_custom_button(__("Sales Order"), function() { frappe.model.open_mapped_doc({ method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.make_order", diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json index 0330e5c85c..a63fc4da69 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2018-05-24 07:18:08.256060", "doctype": "DocType", @@ -79,6 +80,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "fieldname": "to_date", "fieldtype": "Date", "label": "To Date", @@ -129,8 +131,10 @@ "label": "Terms and Conditions Details" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-11-18 19:37:37.151686", + "links": [], + "modified": "2021-06-29 00:30:30.621636", "modified_by": "Administrator", "module": "Manufacturing", "name": "Blanket Order", From 91dcc07e26c647708b5317623ec984d9fa1671d1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 15:58:46 +0530 Subject: [PATCH 280/550] fix: Employee Inactive status implications (#26244) --- erpnext/hr/doctype/attendance/attendance.js | 2 +- erpnext/hr/doctype/attendance/attendance.py | 5 +++++ erpnext/hr/doctype/attendance/attendance_list.js | 3 +++ erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js index c3c3cb82f9..7964078c7f 100644 --- a/erpnext/hr/doctype/attendance/attendance.js +++ b/erpnext/hr/doctype/attendance/attendance.js @@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { return{ query: "erpnext.controllers.queries.employee_query" - } + } } diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f3b8a799b3..3412675d81 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -15,6 +15,7 @@ class Attendance(Document): validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) self.validate_attendance_date() self.validate_duplicate_record() + self.validate_employee_status() self.check_leave_record() def validate_attendance_date(self): @@ -38,6 +39,10 @@ class Attendance(Document): frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( frappe.bold(self.employee), frappe.bold(self.attendance_date))) + def validate_employee_status(self): + if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": + frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) + def check_leave_record(self): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 0c7eafe9c6..9a3bac0eb2 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = { label: __('For Employee'), fieldtype: 'Link', options: 'Employee', + get_query: () => { + return {query: "erpnext.controllers.queries.employee_query"} + }, reqd: 1, onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index e71d81f323..5c7c0a3b09 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -459,6 +459,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): where t1.name = t2.employee and t2.docstatus = 1 + and t1.status != 'Inactive' %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) From 1f10a99910e26c0aca4ac9ba30e3d5f985b992ef Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 15:58:56 +0530 Subject: [PATCH 281/550] fix: Employee Inactive status implications (#26245) --- erpnext/hr/doctype/attendance/attendance.js | 2 +- erpnext/hr/doctype/attendance/attendance.py | 5 +++++ erpnext/hr/doctype/attendance/attendance_list.js | 3 +++ erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js index c3c3cb82f9..7964078c7f 100644 --- a/erpnext/hr/doctype/attendance/attendance.js +++ b/erpnext/hr/doctype/attendance/attendance.js @@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { return{ query: "erpnext.controllers.queries.employee_query" - } + } } diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f3b8a799b3..3412675d81 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -15,6 +15,7 @@ class Attendance(Document): validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) self.validate_attendance_date() self.validate_duplicate_record() + self.validate_employee_status() self.check_leave_record() def validate_attendance_date(self): @@ -38,6 +39,10 @@ class Attendance(Document): frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( frappe.bold(self.employee), frappe.bold(self.attendance_date))) + def validate_employee_status(self): + if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": + frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) + def check_leave_record(self): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 0c7eafe9c6..9a3bac0eb2 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = { label: __('For Employee'), fieldtype: 'Link', options: 'Employee', + get_query: () => { + return {query: "erpnext.controllers.queries.employee_query"} + }, reqd: 1, onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index e71d81f323..5c7c0a3b09 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -459,6 +459,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): where t1.name = t2.employee and t2.docstatus = 1 + and t1.status != 'Inactive' %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) From 9181dde86a9ccabdfb2c075383dc1daaa8f4662f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Jun 2021 17:18:39 +0530 Subject: [PATCH 282/550] fix: Cancelation of Loan Security Pledges --- .../loan_security_pledge.json | 84 ++++++++++++++----- .../loan_security_pledge.py | 18 +++- 2 files changed, 76 insertions(+), 26 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json index 18bd4aea78..68bac8ed8c 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -35,7 +35,9 @@ "no_copy": 1, "options": "Loan Security Pledge", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "loan_application.applicant", @@ -45,47 +47,63 @@ "in_standard_filter": 1, "label": "Applicant", "options": "applicant_type", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan_security_details_section", "fieldtype": "Section Break", - "label": "Loan Security Details" + "label": "Loan Security Details", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan", "fieldtype": "Link", "label": "Loan", - "options": "Loan" + "options": "Loan", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan_application", "fieldtype": "Link", "label": "Loan Application", - "options": "Loan Application" + "options": "Loan Application", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "total_security_value", "fieldtype": "Currency", "label": "Total Security Value", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "maximum_loan_value", "fieldtype": "Currency", "label": "Maximum Loan Value", "options": "Company:company:default_currency", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "loan_details_section", "fieldtype": "Section Break", - "label": "Loan Details" + "label": "Loan Details", + "show_days": 1, + "show_seconds": 1 }, { "default": "Requested", @@ -94,37 +112,49 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "options": "Requested\nUnpledged\nPledged\nPartially Pledged", - "read_only": 1 + "options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "pledge_time", "fieldtype": "Datetime", "label": "Pledge Time", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "securities", "fieldtype": "Table", "label": "Securities", "options": "Pledge", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_11", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_10", "fieldtype": "Section Break", - "label": "Totals" + "label": "Totals", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fetch_from": "loan.applicant_type", @@ -132,35 +162,45 @@ "fieldtype": "Select", "label": "Applicant Type", "options": "Employee\nMember\nCustomer", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "more_information_section", "fieldtype": "Section Break", - "label": "More Information" + "label": "More Information", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "reference_no", "fieldtype": "Data", - "label": "Reference No" + "label": "Reference No", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_18", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "description", "fieldtype": "Text", - "label": "Description" + "label": "Description", + "show_days": 1, + "show_seconds": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:23:16.953305", + "modified": "2021-06-29 17:15:16.082256", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Pledge", diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index cbc8376aa5..c390b6c526 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -23,6 +23,12 @@ class LoanSecurityPledge(Document): update_shortfall_status(self.loan, self.total_security_value) update_loan(self.loan, self.maximum_loan_value) + def on_cancel(self): + if self.loan: + self.db_set("status", "Cancelled") + self.db_set("pledge_time", None) + update_loan(self.loan, self.maximum_loan_value, cancel=1) + def validate_duplicate_securities(self): security_list = [] for security in self.securities: @@ -36,7 +42,7 @@ class LoanSecurityPledge(Document): existing_pledge = '' if self.loan: - existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name']) + existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name']) if existing_pledge: loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type']) @@ -77,8 +83,12 @@ class LoanSecurityPledge(Document): self.total_security_value = total_security_value self.maximum_loan_value = maximum_loan_value -def update_loan(loan, maximum_value_against_pledge): +def update_loan(loan, maximum_value_against_pledge, cancel=0): maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 - WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) + if cancel: + frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s + WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan)) + else: + frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 + WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) From b4b6c8e2620f36212da55206aa3ff2c9e9fe779c Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 29 Jun 2021 17:58:39 +0530 Subject: [PATCH 283/550] Revert "fix: Test" This reverts commit feed97ec53767e777b8a638d5a8e3ffc0a04e500. --- ...tracted_raw_materials_to_be_transferred.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index f1784925c5..2448e17c50 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -60,7 +60,6 @@ def transfer_subcontracted_raw_materials(po): rm_item = [ { 'name': po.supplied_items[0].name, -<<<<<<< HEAD 'item_code': item_1, 'rm_item_code': item_1, 'item_name': item_1, @@ -68,20 +67,10 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_1], -======= - 'item_code': '_Test Item Home Desktop 100', - 'rm_item_code': '_Test Item Home Desktop 100', - 'item_name': '_Test Item Home Desktop 100', - 'qty': 2, - 'warehouse': '_Test Warehouse - _TC', - 'rate': 100, - 'amount': 200, ->>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' }, { 'name': po.supplied_items[1].name, -<<<<<<< HEAD 'item_code': item_2, 'rm_item_code': item_2, 'item_name': item_2, @@ -89,15 +78,6 @@ def transfer_subcontracted_raw_materials(po): 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100 * transfer_qty_map[item_2], -======= - 'item_code': '_Test Item', - 'rm_item_code': '_Test Item', - 'item_name': '_Test Item', - 'qty': 1, - 'warehouse': '_Test Warehouse - _TC', - 'rate': 100, - 'amount': 100, ->>>>>>> c4d851e45f (fix: Test) 'stock_uom': 'Nos' } ] From 61690775a836be80d19d842b7cf7a6e6d56e1dba Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Jun 2021 15:42:39 +0530 Subject: [PATCH 284/550] feat: provision to make subcontracted purchase order from the production plan --- .../purchase_order_item.json | 29 ++- erpnext/manufacturing/doctype/bom/bom.json | 21 +- erpnext/manufacturing/doctype/bom/bom.py | 19 +- .../doctype/bom/bom_item_preview.html | 36 +++- erpnext/manufacturing/doctype/bom/bom_tree.js | 2 +- .../production_plan/production_plan.js | 41 +++- .../production_plan/production_plan.json | 31 ++- .../production_plan/production_plan.py | 180 +++++++++++----- .../production_plan_dashboard.py | 4 + .../production_plan/test_production_plan.py | 10 +- .../production_plan_item.json | 19 +- .../__init__.py | 0 .../production_plan_sub_assembly_item.json | 202 ++++++++++++++++++ .../production_plan_sub_assembly_item.py | 10 + .../doctype/work_order/work_order.json | 18 +- .../doctype/work_order/work_order.py | 3 +- .../work_order/work_order_preview.html | 33 +++ .../report/bom_explorer/bom_explorer.py | 11 +- .../job_card_summary/job_card_summary.js | 12 ++ .../job_card_summary/job_card_summary.json | 8 +- .../production_plan_summary/__init__.py | 0 .../production_plan_summary.js | 32 +++ .../production_plan_summary.json | 26 +++ .../production_plan_summary.py | 136 ++++++++++++ .../work_order_summary/work_order_summary.py | 5 +- erpnext/patches.txt | 1 + erpnext/patches/v13_0/update_level_in_bom.py | 30 +++ .../stock/doctype/stock_entry/stock_entry.py | 2 +- 28 files changed, 809 insertions(+), 112 deletions(-) create mode 100644 erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json create mode 100644 erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py create mode 100644 erpnext/manufacturing/doctype/work_order/work_order_preview.html create mode 100644 erpnext/manufacturing/report/production_plan_summary/__init__.py create mode 100644 erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js create mode 100644 erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json create mode 100644 erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py create mode 100644 erpnext/patches/v13_0/update_level_in_bom.py diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1dbd7c60c3..132dd1769c 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -97,6 +97,9 @@ "is_fixed_asset", "item_tax_rate", "section_break_72", + "production_plan", + "production_plan_item", + "production_plan_sub_assembly_item", "page_break" ], "fields": [ @@ -803,13 +806,37 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "production_plan", + "fieldtype": "Link", + "label": "Production Plan", + "options": "Production Plan", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "production_plan_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Item", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Sub Assembly Item", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-03-22 11:46:12.357435", + "modified": "2021-06-28 19:22:22.715365", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index f38d1b9892..7e539183b0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -36,6 +36,9 @@ "materials_section", "inspection_required", "quality_inspection_template", + "column_break_31", + "bom_level", + "section_break_33", "items", "scrap_section", "scrap_items", @@ -513,6 +516,22 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "bom_level", + "fieldtype": "Int", + "label": "BOM Level", + "read_only": 1 + }, + { + "fieldname": "section_break_33", + "fieldtype": "Section Break", + "hide_border": 1 } ], "icon": "fa fa-sitemap", @@ -520,7 +539,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-03-16 12:25:09.081968", + "modified": "2021-05-16 12:25:09.081968", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c58f017258..c31b1bd3e9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -154,6 +154,7 @@ class BOM(WebsiteGenerator): self.calculate_cost() self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) + self.set_bom_level() def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -676,6 +677,19 @@ class BOM(WebsiteGenerator): """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) + def set_bom_level(self, update=False): + levels = [] + + self.bom_level = 0 + for row in self.items: + if row.bom_no: + levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0) + + if levels: + self.bom_level = max(levels) + 1 + + if update: + self.db_set("bom_level", self.bom_level) def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': @@ -860,7 +874,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): frappe.form_dict.parent = parent if frappe.form_dict.parent: - bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent) + bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent) frappe.has_permission("BOM", doc=bom_doc, throw=True) bom_items = frappe.get_all('BOM Item', @@ -871,7 +885,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): item_names = tuple(d.get('item_code') for d in bom_items) items = frappe.get_list('Item', - fields=['image', 'description', 'name', 'stock_uom', 'item_name'], + fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'], filters=[['name', 'in', item_names]]) # to get only required item dicts for bom_item in bom_items: @@ -884,6 +898,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): bom_item.parent_bom_qty = bom_doc.quantity bom_item.expandable = 0 if bom_item.value in ('', None) else 1 + bom_item.image = frappe.db.escape(bom_item.image) return bom_items diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html index 6cd5f8cb3c..6088e46265 100644 --- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html +++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html @@ -1,13 +1,31 @@
    - {% if data.image %} - -
    - {% endif %} -

    - {{ __("Description") }} -

    -
    - {{ data.description }} +
    +
    + {% if data.image %} +
    + +
    + {% endif %} +
    +
    +

    + {{ __("Description") }} +

    +
    + {{ data.description }} +
    +
    +

    + {% if data.value %} + + {{ __("Open BOM {0}", [data.value.bold()]) }} + {% endif %} + {% if data.item_code %} + + {{ __("Open Item {0}", [data.item_code.bold()]) }} + {% endif %} +

    +

    diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index 185b9ed4bc..60fb377f47 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = { if(node.is_root && node.data.value!="BOM") { frappe.model.with_doc("BOM", node.data.value, function() { var bom = frappe.model.get_doc("BOM", node.data.value); - node.data.image = bom.image || ""; + node.data.image = escape(bom.image) || ""; node.data.description = bom.description || ""; }); } diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 450aa04a73..d198a6962a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Production Plan', { setup: function(frm) { frm.custom_make_buttons = { - 'Work Order': 'Work Order', + 'Work Order': 'Work Order / Subcontract PO', 'Material Request': 'Material Request', }; @@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', { frm.trigger("show_progress"); if (frm.doc.status !== "Completed") { - if (frm.doc.po_items && frm.doc.status !== "Closed") { - frm.add_custom_button(__("Work Order"), ()=> { - frm.trigger("make_work_order"); - }, __('Create')); - } + frm.add_custom_button(__("Work Order Tree"), ()=> { + frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name}); + }, __('View')); - if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { - frm.add_custom_button(__("Material Request"), ()=> { - frm.trigger("make_material_request"); - }, __('Create')); - } + frm.add_custom_button(__("Production Plan Summary"), ()=> { + frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name}); + }, __('View')); if (frm.doc.status === "Closed") { frm.add_custom_button(__("Re-open"), function() { @@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', { frm.events.close_open_production_plan(frm, true); }, __("Status")); } + + if (frm.doc.po_items && frm.doc.status !== "Closed") { + frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> { + frm.trigger("make_work_order"); + }, __('Create')); + } + + if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { + frm.add_custom_button(__("Material Request"), ()=> { + frm.trigger("make_material_request"); + }, __('Create')); + } } } @@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', { }); }, + get_sub_assembly_items: function(frm) { + frappe.call({ + method: "get_sub_assembly_items", + freeze: true, + doc: frm.doc, + callback: function() { + refresh_field("sub_assembly_items"); + } + }); + }, + get_items_for_mr: function(frm) { if (!frm.doc.for_warehouse) { frappe.throw(__("Select warehouse for material requests")); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 1c0dde227c..84378956c6 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -32,6 +32,9 @@ "po_items", "section_break_25", "prod_plan_references", + "section_break_24", + "get_sub_assembly_items", + "sub_assembly_items", "material_request_planning", "include_non_stock_items", "include_subcontracted_items", @@ -187,7 +190,7 @@ "depends_on": "get_items_from", "fieldname": "get_items", "fieldtype": "Button", - "label": "Get Items For Work Order" + "label": "Get Finished Goods for Manufacture" }, { "fieldname": "po_items", @@ -199,7 +202,7 @@ { "fieldname": "material_request_planning", "fieldtype": "Section Break", - "label": "Material Request Planning" + "label": "Material Requirement Planning" }, { "default": "1", @@ -237,12 +240,13 @@ }, { "fieldname": "section_break_27", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "mr_items", "fieldtype": "Table", - "label": "Material Request Plan Item", + "label": "Raw Materials", "no_copy": 1, "options": "Material Request Plan Item" }, @@ -337,13 +341,30 @@ "hidden": 1, "label": "Production Plan Item Reference", "options": "Production Plan Item Reference" + }, + { + "fieldname": "section_break_24", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "sub_assembly_items", + "fieldtype": "Table", + "label": "Sub Assembly Items", + "no_copy": 1, + "options": "Production Plan Sub Assembly Item" + }, + { + "fieldname": "get_sub_assembly_items", + "fieldtype": "Button", + "label": "Get Sub Assembly Items" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-24 16:59:03.643211", + "modified": "2021-06-28 20:00:33.905114", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0ede1bd4ab..38a0ee77ad 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -5,10 +5,11 @@ from __future__ import unicode_literals import frappe, json, copy from frappe import msgprint, _ -from six import string_types, iteritems +from six import iteritems from frappe.model.document import Document -from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil +from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime, + ceil, get_link_to_form, getdate) from frappe.utils.csvutils import build_csv_response from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children from erpnext.manufacturing.doctype.work_order.work_order import get_item_details @@ -349,49 +350,88 @@ class ProductionPlan(Document): @frappe.whitelist() def make_work_order(self): - wo_list = [] + wo_list, po_list = [], [] + subcontracted_po = {} + self.validate_data() + self.make_work_order_for_finished_goods(wo_list) + self.make_work_order_for_subassembly_items(wo_list, subcontracted_po) + self.make_subcontracted_purchase_order(subcontracted_po, po_list) + self.show_list_created_message('Work Order', wo_list) + self.show_list_created_message('Purchase Order', po_list) + + def make_work_order_for_finished_goods(self, wo_list): items_data = self.get_production_items() for key, item in items_data.items(): + if self.sub_assembly_items: + item['use_multi_level_bom'] = 0 + work_order = self.create_work_order(item) if work_order: wo_list.append(work_order) - if item.get("make_work_order_for_sub_assembly_items"): - work_orders = self.make_work_order_for_sub_assembly_items(item) - wo_list.extend(work_orders) + def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po): + for row in self.sub_assembly_items: + if row.type_of_manufacturing == 'Subcontract': + subcontracted_po.setdefault(row.supplier, []).append(row) + continue + + args = {} + self.prepare_args_for_sub_assembly_items(row, args) + work_order = self.create_work_order(args) + if work_order: + wo_list.append(work_order) + + def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders): + if not subcontracted_po: + return + + for supplier, po_list in subcontracted_po.items(): + po = frappe.new_doc('Purchase Order') + po.supplier = supplier + po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() + po.is_subcontracted_item = 'Yes' + for row in po_list: + args = { + 'item_code': row.production_item, + 'warehouse': row.fg_warehouse, + 'production_plan_sub_assembly_item': row.name, + 'bom': row.bom_no, + 'production_plan': self.name + } + + for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name', + 'description', 'production_plan_item']: + args[field] = row.get(field) + + po.append('items', args) + + po.set_missing_values() + po.flags.ignore_mandatory = True + po.flags.ignore_validate = True + po.insert() + purchase_orders.append(po.name) + + def show_list_created_message(self, doctype, doc_list=None): + if not doc_list: + return frappe.flags.mute_messages = False + if doc_list: + doc_list = [get_link_to_form(doctype, p) for p in doc_list] + msgprint(_("{0} created").format(comma_and(doc_list))) - if wo_list: - wo_list = ["""%s""" % \ - (p, p) for p in wo_list] - msgprint(_("{0} created").format(comma_and(wo_list))) - else : - msgprint(_("No Work Orders created")) + def prepare_args_for_sub_assembly_items(self, row, args): + for field in ["production_item", "item_name", "qty", "fg_warehouse", + "description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]: + args[field] = row.get(field) - def make_work_order_for_sub_assembly_items(self, item): - work_orders = [] - bom_data = {} - - get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty")) - - for key, data in bom_data.items(): - data.update({ - 'qty': data.get("stock_qty"), - 'production_plan': self.name, - 'use_multi_level_bom': item.get("use_multi_level_bom"), - 'company': self.company, - 'fg_warehouse': item.get("fg_warehouse"), - 'update_consumed_material_cost_in_project': 0 - }) - - work_order = self.create_work_order(data) - if work_order: - work_orders.append(work_order) - - return work_orders + args.update({ + "use_multi_level_bom": 0, + "production_plan": self.name, + "production_plan_sub_assembly_item": row.name + }) def create_work_order(self, item): from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse @@ -476,9 +516,32 @@ class ProductionPlan(Document): else : msgprint(_("No material request created")) + @frappe.whitelist() + def get_sub_assembly_items(self, manufacturing_type=None): + self.sub_assembly_items = [] + for row in self.po_items: + bom_data = [] + get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) + self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) + + self.save() + + def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): + bom_data = sorted(bom_data, key = lambda i: i.bom_level) + + for data in bom_data: + data.qty = data.stock_qty + data.production_plan_item = row.name + data.fg_warehouse = row.warehouse + data.schedule_date = row.planned_start_date + data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item + else "In House") + + self.append("sub_assembly_items", data) + @frappe.whitelist() def download_raw_materials(doc, warehouses=None): - if isinstance(doc, string_types): + if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', @@ -660,7 +723,7 @@ def get_sales_orders(self): @frappe.whitelist() def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): - if isinstance(row, string_types): + if isinstance(row, str): row = frappe._dict(json.loads(row)) company = frappe.db.escape(company) @@ -684,8 +747,11 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) -def get_warehouse_list(warehouses, warehouse_list=[]): - if isinstance(warehouses, string_types): +def get_warehouse_list(warehouses, warehouse_list=None): + if not warehouse_list: + warehouse_list = [] + + if isinstance(warehouses, str): warehouses = json.loads(warehouses) for row in warehouses: @@ -697,7 +763,7 @@ def get_warehouse_list(warehouses, warehouse_list=[]): @frappe.whitelist() def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): - if isinstance(doc, string_types): + if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) warehouse_list = [] @@ -726,6 +792,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d so_item_details = frappe._dict() for data in po_items: + if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): + data["include_exploded_items"] = 1 + planned_qty = data.get('required_qty') or data.get('planned_qty') ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty warehouse = doc.get('for_warehouse') @@ -857,23 +926,28 @@ def get_item_data(item_code): # "description": item_details.get("description") } -def get_sub_assembly_items(bom_no, bom_data, to_produce_qty): +def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): data = get_children('BOM', parent = bom_no) for d in data: if d.expandable: - key = (d.name, d.value) - if key not in bom_data: - bom_data.setdefault(key, { - 'stock_qty': 0, - 'description': d.description, - 'production_item': d.item_code, - 'item_name': d.item_name, - 'stock_uom': d.stock_uom, - 'uom': d.stock_uom, - 'bom_no': d.value - }) + parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") + bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level") + if d.value else 0) - bom_item = bom_data.get(key) - bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) + stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) + bom_data.append(frappe._dict({ + 'parent_item_code': parent_item_code, + 'description': d.description, + 'production_item': d.item_code, + 'item_name': d.item_name, + 'stock_uom': d.stock_uom, + 'uom': d.stock_uom, + 'bom_no': d.value, + 'is_sub_contracted_item': d.is_sub_contracted_item, + 'bom_level': bom_level, + 'indent': indent, + 'stock_qty': stock_qty + })) - get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"]) + if d.value: + get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py index 09ec24a67a..ca597f6327 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py @@ -9,5 +9,9 @@ def get_data(): 'label': _('Transactions'), 'items': ['Work Order', 'Material Request'] }, + { + 'label': _('Subcontract'), + 'items': ['Purchase Order'] + }, ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 768f99eb43..cce1bb61b6 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -169,7 +169,7 @@ class TestProductionPlan(unittest.TestCase): pln.get_items() pln.submit() - self.assertTrue(pln.po_items[0].planned_qty, 3) + self.assertTrue(pln.po_items[0].planned_qty, 3) pln.make_work_order() work_order = frappe.db.get_value('Work Order', { @@ -193,10 +193,10 @@ class TestProductionPlan(unittest.TestCase): for so_item in so_items: so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') self.assertEqual(so_wo_qty, 0.0) - + latest_plan = frappe.get_doc('Production Plan', pln.name) latest_plan.cancel() - + def test_pp_to_mr_customer_provided(self): #Material Request from Production Plan for Customer Provided create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) @@ -236,10 +236,10 @@ class TestProductionPlan(unittest.TestCase): pln.append("po_items", { "item_code": item_code, "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}), - "planned_qty": 3, - "make_work_order_for_sub_assembly_items": 1 + "planned_qty": 3 }) + pln.get_sub_assembly_items('In House') pln.submit() pln.make_work_order() diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json index 89ab7aa0a0..f829d57475 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -9,18 +9,17 @@ "include_exploded_items", "item_code", "bom_no", - "planned_qty", "column_break_6", - "make_work_order_for_sub_assembly_items", + "planned_qty", "warehouse", "planned_start_date", "section_break_9", "pending_qty", "ordered_qty", - "produced_qty", "column_break_17", "description", "stock_uom", + "produced_qty", "reference_section", "sales_order", "sales_order_item", @@ -32,11 +31,10 @@ ], "fields": [ { - "columns": 2, - "default": "0", + "columns": 1, + "default": "1", "fieldname": "include_exploded_items", "fieldtype": "Check", - "in_list_view": 1, "label": "Include Exploded Items" }, { @@ -80,13 +78,6 @@ "fieldname": "column_break_6", "fieldtype": "Column Break" }, - { - "default": "0", - "description": "If enabled, system will create the work order for the exploded items against which BOM is available.", - "fieldname": "make_work_order_for_sub_assembly_items", - "fieldtype": "Check", - "label": "Make Work Order for Sub Assembly Items" - }, { "fieldname": "warehouse", "fieldtype": "Link", @@ -218,7 +209,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-04-28 19:14:57.772123", + "modified": "2021-06-28 18:31:06.822168", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json new file mode 100644 index 0000000000..657ee35a85 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -0,0 +1,202 @@ +{ + "actions": [], + "creation": "2020-12-27 16:08:36.127199", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "production_item", + "item_name", + "fg_warehouse", + "parent_item_code", + "schedule_date", + "column_break_3", + "qty", + "bom_no", + "bom_level", + "type_of_manufacturing", + "supplier", + "work_order_details_section", + "work_order", + "purchase_order", + "production_plan_item", + "column_break_7", + "produced_qty", + "received_qty", + "indent", + "section_break_19", + "uom", + "stock_uom", + "column_break_22", + "description" + ], + "fields": [ + { + "fetch_from": "sub_assembly_item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.type_of_manufacturing == \"In House\"", + "fieldname": "work_order_details_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "work_order", + "fieldtype": "Link", + "label": "Work Order", + "options": "Work Order", + "read_only": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty", + "read_only": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "read_only": 1 + }, + { + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty" + }, + { + "fieldname": "bom_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Bom No", + "options": "BOM" + }, + { + "fieldname": "production_plan_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Item", + "read_only": 1 + }, + { + "fieldname": "parent_item_code", + "fieldtype": "Link", + "label": "Finished Good", + "options": "Item", + "read_only": 1 + }, + { + "columns": 1, + "fetch_from": "bom_no.bom_level", + "fieldname": "bom_level", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Level (BOM)", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_19", + "fieldtype": "Section Break", + "label": "Item Details" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "description", + "read_only": 1 + }, + { + "fieldname": "production_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sub Assembly Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "indent", + "fieldtype": "Int", + "label": "Indent" + }, + { + "fieldname": "fg_warehouse", + "fieldtype": "Link", + "label": "Target Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "produced_qty", + "fieldtype": "Data", + "label": "Produced Quantity", + "read_only": 1 + }, + { + "default": "In House", + "fieldname": "type_of_manufacturing", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Manufacturing Type", + "options": "In House\nSubcontract" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'", + "options": "Supplier" + }, + { + "fieldname": "schedule_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Schedule Date" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-06-28 20:10:56.296410", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Sub Assembly Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py new file mode 100644 index 0000000000..6850a2eb4e --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ProductionPlanSubAssemblyItem(Document): + pass diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 44d76d2b01..3b56854aaf 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -64,11 +64,16 @@ "description", "stock_uom", "column_break2", + "references_section", "material_request", "material_request_item", "sales_order_item", + "column_break_61", "production_plan", "production_plan_item", + "production_plan_sub_assembly_item", + "parent_work_order", + "bom_level", "product_bundle_item", "amended_from" ], @@ -546,17 +551,26 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 - } + }, + { + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "label": "Production Plan Sub-assembly Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } ], "icon": "fa fa-cogs", "idx": 1, "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-06-20 15:19:14.902699", + "modified": "2021-06-28 16:19:14.902699", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", + "nsm_parent_field": "parent_work_order", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 180815d80e..779ae42d65 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -483,7 +483,7 @@ class WorkOrder(Document): self.set('operations', []) - if not self.bom_no: + if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'): return operations = [] @@ -590,6 +590,7 @@ class WorkOrder(Document): def validate_operation_time(self): for d in self.operations: if not d.time_in_mins > 0: + print(self.bom_no, self.production_item) frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation)) def update_required_items(self): diff --git a/erpnext/manufacturing/doctype/work_order/work_order_preview.html b/erpnext/manufacturing/doctype/work_order/work_order_preview.html new file mode 100644 index 0000000000..a4bf93edef --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order/work_order_preview.html @@ -0,0 +1,33 @@ +

    +
    +
    + {% if data.image %} +
    + +
    + {% endif %} +
    +
    +
    + Status {{ data.status }} +
    +
    + Qty to Produce {{ data.qty }} +
    +
    + Produced Qty {{ data.produced_qty }} +
    +
    +

    + {% if data.value %} + + {{ __("Open Work Order {0}", [data.value.bold()]) }} + {% endif %} + {% if data.item_code %} + + {{ __("Open Item {0}", [data.item_code.bold()]) }} + {% endif %} +

    +
    +
    +
    \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 48907adc5f..858b5546b0 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -20,17 +20,20 @@ def get_exploded_items(bom, data, indent=0, qty=1): fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom']) for item in exploded_items: + print(item.bom_no, indent) item["indent"] = indent data.append({ 'item_code': item.item_code, 'item_name': item.item_name, 'indent': indent, + 'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level") + if item.bom_no else ""), 'bom': item.bom_no, 'qty': item.qty * qty, 'uom': item.uom, 'description': item.description, 'scrap': item.scrap - }) + }) if item.bom_no: get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty) @@ -68,6 +71,12 @@ def get_columns(): "fieldname": "uom", "width": 100 }, + { + "label": "BOM Level", + "fieldtype": "Data", + "fieldname": "bom_level", + "width": 100 + }, { "label": "Standard Description", "fieldtype": "data", diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js index bd68db190e..cb771e4994 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = { get_data: function(txt) { return frappe.db.get_link_options('Item', txt); } + }, + { + label: __("Workstation"), + fieldname: "workstation", + fieldtype: "Link", + options: "Workstation" + }, + { + label: __("Operation"), + fieldname: "operation", + fieldtype: "Link", + options: "Operation" } ] }; diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json index 9f08fc34cb..ecf2b74bbe 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json @@ -1,14 +1,16 @@ { - "add_total_row": 0, + "add_total_row": 1, + "columns": [], "creation": "2020-04-20 12:00:21.436619", "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 0, "is_standard": "Yes", - "letter_head": "Gadgets International", - "modified": "2020-04-20 12:00:21.436619", + "letter_head": "", + "modified": "2020-12-30 11:49:21.713561", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Summary", diff --git a/erpnext/manufacturing/report/production_plan_summary/__init__.py b/erpnext/manufacturing/report/production_plan_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js new file mode 100644 index 0000000000..59396fef16 --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js @@ -0,0 +1,32 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Production Plan Summary"] = { + "filters": [ + { + fieldname: "production_plan", + label: __("Production Plan"), + fieldtype: "Link", + options: "Production Plan", + reqd: 1, + get_query: function() { + return { + filters: { + "docstatus": 1 + } + }; + } + } + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "document_name") { + var color = data.pending_qty > 0 ? 'red': 'green'; + value = `${data['document_name']}`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json new file mode 100644 index 0000000000..33aca21a6e --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2020-12-27 11:43:39.781793", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2020-12-27 11:43:42.677584", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Production Plan", + "report_name": "Production Plan Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py new file mode 100644 index 0000000000..81b1791ae8 --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -0,0 +1,136 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import flt + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_column(filters) + + return columns, data + +def get_data(filters): + data = [] + + order_details = {} + get_work_order_details(filters, order_details) + get_purchase_order_details(filters, order_details) + get_production_plan_item_details(filters, data, order_details) + + return data + +def get_production_plan_item_details(filters, data, order_details): + itemwise_indent = {} + + production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan")) + for row in production_plan_doc.po_items: + work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name, + "bom_no": row.bom_no, "production_item": row.item_code}, "name") + + if row.item_code not in itemwise_indent: + itemwise_indent.setdefault(row.item_code, {}) + + data.append({ + "indent": 0, + "item_code": row.item_code, + "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "qty": row.planned_qty, + "document_type": "Work Order", + "document_name": work_order, + "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"), + "produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"), + "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty")) + }) + + get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details) + +def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details): + for item in production_plan_doc.sub_assembly_items: + if row.name == item.production_plan_item: + subcontracted_item = (item.type_of_manufacturing == 'Subcontract') + + if subcontracted_item: + docname = frappe.get_cached_value("Purchase Order Item", + {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent") + else: + docname = frappe.get_cached_value("Work Order", + {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name") + + data.append({ + "indent": 1, + "item_code": item.production_item, + "item_name": item.item_name, + "qty": item.qty, + "document_type": "Work Order" if not subcontracted_item else "Purchase Order", + "document_name": docname, + "bom_level": item.bom_level, + "produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"), + "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty")) + }) + +def get_work_order_details(filters, order_details): + for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")}, + fields=["name", "produced_qty", "production_plan", "production_item"]): + order_details.setdefault((row.name, row.production_item), row) + +def get_purchase_order_details(filters, order_details): + for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")}, + fields=["parent", "received_qty as produced_qty", "item_code"]): + order_details.setdefault((row.parent, row.item_code), row) + +def get_column(filters): + return [ + { + "label": "Finished Good", + "fieldtype": "Link", + "fieldname": "item_code", + "width": 300, + "options": "Item" + }, + { + "label": "Item Name", + "fieldtype": "data", + "fieldname": "item_name", + "width": 100 + }, + { + "label": "Document Type", + "fieldtype": "Link", + "fieldname": "document_type", + "width": 150, + "options": "DocType" + }, + { + "label": "Document Name", + "fieldtype": "Dynamic Link", + "fieldname": "document_name", + "width": 150 + }, + { + "label": "BOM Level", + "fieldtype": "Int", + "fieldname": "bom_level", + "width": 100 + }, + { + "label": "Order Qty", + "fieldtype": "Float", + "fieldname": "qty", + "width": 120 + }, + { + "label": "Received Qty", + "fieldtype": "Float", + "fieldname": "produced_qty", + "width": 160 + }, + { + "label": "Pending Qty", + "fieldtype": "Float", + "fieldname": "pending_qty", + "width": 110 + } + ] diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index fb047b230c..612dad0bf5 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -19,7 +19,7 @@ def execute(filters=None): return columns, data, None, chart_data def get_data(filters): - query_filters = {"docstatus": 1} + query_filters = {"docstatus": ("<", 2)} fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty", "planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"] @@ -62,7 +62,8 @@ def get_chart_based_on_status(data): "Not Started": 0, "In Process": 0, "Stopped": 0, - "Completed": 0 + "Completed": 0, + "Draft": 0 } for d in data: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2b1fc43a1c..29376f00a1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -290,3 +290,4 @@ erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details +erpnext.patches.v13_0.update_level_in_bom #1234sswef diff --git a/erpnext/patches/v13_0/update_level_in_bom.py b/erpnext/patches/v13_0/update_level_in_bom.py new file mode 100644 index 0000000000..0d03c42e98 --- /dev/null +++ b/erpnext/patches/v13_0/update_level_in_bom.py @@ -0,0 +1,30 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + for document in ["bom", "bom_item", "bom_explosion_item"]: + frappe.reload_doc('manufacturing', 'doctype', document) + + frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1") + + bom_list = frappe.db.sql_list("""select name from `tabBOM` bom + where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item` + where parent=bom.name and ifnull(bom_no, '')!='')""") + + count = 0 + while(count < len(bom_list)): + for parent_bom in get_parent_boms(bom_list[count]): + bom_doc = frappe.get_cached_doc("BOM", parent_bom) + bom_doc.set_bom_level(update=True) + bom_list.append(parent_bom) + count += 1 + +def get_parent_boms(bom_no): + return frappe.db.sql_list(""" + select distinct bom_item.parent from `tabBOM Item` bom_item + where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM' + and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1) + """, bom_no) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8f27ef4356..e21a80030a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -72,7 +72,7 @@ class StockEntry(StockController): self.validate_with_material_request() self.validate_batch() self.validate_inspection() - self.validate_fg_completed_qty() + # self.validate_fg_completed_qty() self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() From 46b67b901b55f296bef953f85059bfc9c788ddbb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Jun 2021 00:03:32 +0530 Subject: [PATCH 285/550] fix: incorrect valuation rate in stock reconciliation --- .../stock_reconciliation.py | 7 +-- .../test_stock_reconciliation.py | 44 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index e646600752..dd94e7c752 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -404,17 +404,18 @@ class StockReconciliation(StockController): key = (d.item_code, d.warehouse) if key not in merge_similar_entries: + d.total_amount = (d.actual_qty * d.valuation_rate) merge_similar_entries[key] = d elif d.serial_no: data = merge_similar_entries[key] data.actual_qty += d.actual_qty data.qty_after_transaction += d.qty_after_transaction - data.valuation_rate = (data.valuation_rate + d.valuation_rate) / data.actual_qty + data.total_amount += (d.actual_qty * d.valuation_rate) + data.valuation_rate = (data.total_amount) / data.actual_qty data.serial_no += '\n' + d.serial_no - if data.incoming_rate: - data.incoming_rate = (data.incoming_rate + d.incoming_rate) / data.actual_qty + data.incoming_rate = (data.total_amount) / data.actual_qty for key, value in merge_similar_entries.items(): new_sl_entries.append(value) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 36380b838b..ce4cbd259c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, unittest -from frappe.utils import flt, nowdate, nowtime +from frappe.utils import flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items @@ -150,6 +150,42 @@ class TestStockReconciliation(unittest.TestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() + + def test_stock_reco_for_merge_serialized_item(self): + to_delete_records = [] + + # Add new serial nos + serial_item_code = "Stock-Reco-Serial-Item-2" + serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" + + sr = create_stock_reconciliation(item_code=serial_item_code, serial_no=random_string(6), + warehouse = serial_warehouse, qty=1, rate=100, do_not_submit=True, purpose='Opening Stock') + + for i in range(3): + sr.append('items', { + 'item_code': serial_item_code, + 'warehouse': serial_warehouse, + 'qty': 1, + 'valuation_rate': 100, + 'serial_no': random_string(6) + }) + + sr.save() + sr.submit() + + sle_entries = frappe.get_all('Stock Ledger Entry', filters= {'voucher_no': sr.name}, + fields = ['name', 'incoming_rate']) + + self.assertEqual(len(sle_entries), 1) + self.assertEqual(sle_entries[0].incoming_rate, 100) + + to_delete_records.append(sr.name) + to_delete_records.reverse() + + for d in to_delete_records: + stock_doc = frappe.get_doc("Stock Reconciliation", d) + stock_doc.cancel() + def test_stock_reco_for_batch_item(self): to_delete_records = [] to_delete_serial_nos = [] @@ -231,6 +267,12 @@ def create_batch_or_serial_no_items(): serial_item_doc.serial_no_series = "SRSI.####" serial_item_doc.save(ignore_permissions=True) + serial_item_doc = create_item("Stock-Reco-Serial-Item-2", is_stock_item=1) + if not serial_item_doc.has_serial_no: + serial_item_doc.has_serial_no = 1 + serial_item_doc.serial_no_series = "SRSII.####" + serial_item_doc.save(ignore_permissions=True) + batch_item_doc = create_item("Stock-Reco-batch-Item-1", is_stock_item=1) if not batch_item_doc.has_batch_no: batch_item_doc.has_batch_no = 1 From 815e6ec846799449bf15b4c70b95bf3532e34a4f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Jun 2021 11:35:50 +0530 Subject: [PATCH 286/550] fix: minor removed unused file --- .../work_order/work_order_preview.html | 33 ------------------- .../stock/doctype/stock_entry/stock_entry.py | 2 +- 2 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/work_order/work_order_preview.html diff --git a/erpnext/manufacturing/doctype/work_order/work_order_preview.html b/erpnext/manufacturing/doctype/work_order/work_order_preview.html deleted file mode 100644 index a4bf93edef..0000000000 --- a/erpnext/manufacturing/doctype/work_order/work_order_preview.html +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    -
    - {% if data.image %} -
    - -
    - {% endif %} -
    -
    -
    - Status {{ data.status }} -
    -
    - Qty to Produce {{ data.qty }} -
    -
    - Produced Qty {{ data.produced_qty }} -
    -
    -

    - {% if data.value %} - - {{ __("Open Work Order {0}", [data.value.bold()]) }} - {% endif %} - {% if data.item_code %} - - {{ __("Open Item {0}", [data.item_code.bold()]) }} - {% endif %} -

    -
    -
    -
    \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e21a80030a..8f27ef4356 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -72,7 +72,7 @@ class StockEntry(StockController): self.validate_with_material_request() self.validate_batch() self.validate_inspection() - # self.validate_fg_completed_qty() + self.validate_fg_completed_qty() self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() From cf7af62b053ed2ffbe39054b64aafc49557e87b9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Jun 2021 14:14:32 +0530 Subject: [PATCH 287/550] fix: Code cleanup --- .../purchase_invoice/purchase_invoice.py | 32 +++++++++---------- .../purchase_invoice/test_purchase_invoice.py | 23 ++----------- .../purchase_receipt/purchase_receipt.py | 6 ++-- .../purchase_receipt/test_purchase_receipt.py | 15 ++++++--- 4 files changed, 31 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c164868678..c1cc092554 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): if d.category in ('Valuation', 'Total and Valuation') and flt(d.base_tax_amount_after_discount_amount)] - exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) + exchange_rate_map, net_rate_map = get_purchase_document_details(self) for item in self.get("items"): if flt(item.base_net_amount): @@ -640,7 +640,7 @@ class PurchaseInvoice(BuyingController): if item.get('purchase_receipt'): if exchange_rate_map[item.purchase_receipt] and \ self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \ - item.net_rate == net_rate_map[item.item_code]: + item.net_rate == net_rate_map[item.pr_detail]: discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \ (exchange_rate_map[item.purchase_receipt] - self.conversion_rate) @@ -1172,32 +1172,32 @@ class PurchaseInvoice(BuyingController): self.db_set('status', self.status, update_modified = update_modified) # to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling -def get_pr_or_pi_details(doc): +def get_purchase_document_details(doc): if doc.doctype == 'Purchase Invoice': - pr_or_pi = 'purchase_receipt' + doc_reference = 'purchase_receipt' items_reference = 'pr_detail' - pr_or_pi_doctype = 'Purchase Receipt' - pr_or_pi_items_table = 'Purchase Receipt Item' + parent_doctype = 'Purchase Receipt' + child_doctype = 'Purchase Receipt Item' else: - pr_or_pi = 'purchase_invoice' + doc_reference = 'purchase_invoice' items_reference = 'purchase_invoice_item' - pr_or_pi_doctype = 'Purchase Invoice' - pr_or_pi_items_table = 'Purchase Invoice Item' + parent_doctype = 'Purchase Invoice' + child_doctype = 'Purchase Invoice Item' purchase_receipts_or_invoices = [] - pr_or_pi_items = [] + items = [] for item in doc.get('items'): - if item.get(pr_or_pi): - purchase_receipts_or_invoices.append(item.get(pr_or_pi)) + if item.get(doc_reference): + purchase_receipts_or_invoices.append(item.get(doc_reference)) if item.get(items_reference): - pr_or_pi_items.append(item.get(items_reference)) + items.append(item.get(items_reference)) - exchange_rate_map = frappe._dict(frappe.get_all(pr_or_pi_doctype, filters={'name': ('in', + exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in', purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1)) - net_rate_map = frappe._dict(frappe.get_all(pr_or_pi_items_table, filters={'name': ('in', - pr_or_pi_items)}, fields=['item_code', 'net_rate'], as_list=1)) + net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in', + items)}, fields=['name', 'net_rate'], as_list=1)) return exchange_rate_map, net_rate_map diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index c38b9017c8..ec93314c0f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -231,12 +231,11 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_purchase_invoice_with_exchange_rate_difference(self): - set_gst_settings() pr = make_purchase_receipt(currency = "USD", conversion_rate = 70) pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True") - for item in pi.items: - item.purchase_receipt = pr.name + pi.items[0].purchase_receipt = pr.name + pi.items[0].pr_detail = pr.items[0].name pi.insert() pi.submit() @@ -1072,24 +1071,6 @@ def update_tax_witholding_category(company, account, date): 'account': account }) tds_category.save() -def set_gst_settings(): - gst_settings = frappe.get_doc("GST Settings") - - gst_account = frappe.get_all( - "GST Account", - fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"} - ) - - if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company", - "cgst_account": "CGST - _TC", - "sgst_account": "SGST - _TC", - "igst_account": "IGST - _TC", - }) - - gst_settings.save() def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 12fa5302d5..5ba9c7057b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -254,7 +254,7 @@ class PurchaseReceipt(BuyingController): return process_gl_map(gl_entries) def make_item_gl_entries(self, gl_entries, warehouse_account=None): - from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_pr_or_pi_details + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_purchase_document_details stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) @@ -264,7 +264,7 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() - exchange_rate_map, net_rate_map = get_pr_or_pi_details(self) + exchange_rate_map, net_rate_map = get_purchase_document_details(self) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): @@ -312,7 +312,7 @@ class PurchaseReceipt(BuyingController): if d.get('purchase_invoice'): if exchange_rate_map[d.purchase_invoice] and \ self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \ - d.net_rate == net_rate_map[d.item_code]: + d.net_rate == net_rate_map[d.purchase_invoice_item]: discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \ (exchange_rate_map[d.purchase_invoice] - self.conversion_rate) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0ce4c3a669..d56822a308 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1051,6 +1051,7 @@ class TestPurchaseReceipt(unittest.TestCase): self.assertEqual(len(item_two_gl_entry), 1) frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value) + def test_purchase_receipt_with_exchange_rate_difference(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice @@ -1058,22 +1059,26 @@ class TestPurchaseReceipt(unittest.TestCase): create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", properties={"account": '_Test Account Stock In Hand - TCP1'}) + pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80, do_not_save = "True") - for item in pr.items: - item.purchase_invoice = pi.name + pr.items[0].purchase_invoice = pi.name + pr.items[0].purchase_invoice_item = pi.items[0].name pr.insert() - pr.submit() + pr.submit() # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'}) - voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') - + voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') self.assertEqual(pr.name, voucher_no) + exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) + self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s From 8492bf040dc5e309c74e9c51121ed6eff519367b Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 30 Jun 2021 17:17:43 +0530 Subject: [PATCH 288/550] fix: feating employee in payroll entry --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 5c7c0a3b09..36e728fc99 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -680,6 +680,10 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] include_employees = [] emp_cond = '' + + if not filters.payroll_frequency: + frappe.throw(_('Select Payroll Frequency.')) + if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) emp = filters.get('employees') From cf4e29a604c819f0673876592c2c9219a1830d0b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 30 Jun 2021 20:27:32 +0530 Subject: [PATCH 289/550] chore: Added change log for v13.6.0 --- erpnext/change_log/v13/v13_6_0.md | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 erpnext/change_log/v13/v13_6_0.md diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md new file mode 100644 index 0000000000..d881b279e3 --- /dev/null +++ b/erpnext/change_log/v13/v13_6_0.md @@ -0,0 +1,72 @@ +# Version 13.6.0 Release Notes + +### Features & Enhancements + +- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523)) +- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044)) +- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184)) +- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878)) +- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705)) +- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030)) +- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696)) +- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891)) + +### Fixes + +- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) +- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176)) +- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092)) +- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978)) +- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073)) +- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245)) +- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230)) +- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125)) +- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134)) +- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196)) +- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083)) +- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941)) +- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945)) +- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011)) +- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070)) +- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071)) +- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122)) +- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220)) +- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003)) +- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229)) +- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269)) +- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045)) +- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170)) +- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032)) +- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095)) +- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023)) +- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191)) +- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188)) +- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217)) +- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152)) +- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108)) +- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202)) +- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906)) +- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894)) +- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997)) +- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051)) +- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043)) +- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143)) +- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211)) +- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126)) +- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192)) +- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081)) +- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187)) +- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195)) +- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947)) +- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951)) +- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968)) +- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037)) +- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198)) +- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100)) +- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098)) +- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062)) +- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031)) +- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203)) +- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185)) +- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934)) +- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201)) From 68c697b354367e394e00a451487cd24528514d40 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 09:31:31 +0530 Subject: [PATCH 290/550] fix: Auto process deferred accounting for multi-company setup --- erpnext/accounts/deferred_revenue.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 2f86c6c1de..335e8a15ab 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None): start_date = add_months(today(), -1) end_date = add_days(today(), -1) - for record_type in ('Income', 'Expense'): - doc = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=posting_date, - start_date=start_date, - end_date=end_date, - type=record_type - )) + companies = frappe.get_all('Company') - doc.insert() - doc.submit() + for company in companies: + for record_type in ('Income', 'Expense'): + doc = frappe.get_doc(dict( + doctype='Process Deferred Accounting', + company=company.name, + posting_date=posting_date, + start_date=start_date, + end_date=end_date, + type=record_type + )) + + doc.insert() + doc.submit() def make_gl_entries(doc, credit_account, debit_account, against, amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): From 0bfd56e615f0e9f9053f572d945bbc15f3948dd6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 1 Jul 2021 11:50:48 +0530 Subject: [PATCH 291/550] fix: update cost not working in the draft bom --- erpnext/manufacturing/doctype/bom/bom.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 27019dbbae..15a7c316c9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -325,8 +325,7 @@ frappe.ui.form.on("BOM", { freeze: true, args: { update_parent: true, - from_child_bom:false, - save: frm.doc.docstatus === 1 ? true : false + from_child_bom:false }, callback: function(r) { refresh_field("items"); From a856624ccb9c20b3cf4428204a0da6840df767c2 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Jun 2021 23:27:24 +0530 Subject: [PATCH 292/550] fix: employee selection not working in payroll entry --- .../doctype/payroll_entry/payroll_entry.js | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index f2892600d1..496c37b2fa 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', { }); frm.set_query('employee', 'employees', () => { - if (!frm.doc.company) { - frappe.msgprint(__("Please set a Company")); - return []; + let error_fields = []; + let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date']; + + let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]); + + mandatory_fields.forEach(field => { + if (!frm.doc[field]) { + error_fields.push(frappe.unscrub(field)); + } + }); + + if (error_fields && error_fields.length) { + message = message + '

    • ' + error_fields.join('
    • ') + "
    "; + frappe.throw({ + message: message, + indicator: 'red', + title: __('Missing Fields') + }); } + return { query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", filters: frm.events.get_employee_filters(frm) @@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', { get_employee_filters: function (frm) { let filters = {}; - filters['company'] = frm.doc.company; - filters['start_date'] = frm.doc.start_date; - filters['end_date'] = frm.doc.end_date; filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet; - filters['payroll_frequency'] = frm.doc.payroll_frequency; - filters['payroll_payable_account'] = frm.doc.payroll_payable_account; - filters['currency'] = frm.doc.currency; - if (frm.doc.department) { - filters['department'] = frm.doc.department; - } - if (frm.doc.branch) { - filters['branch'] = frm.doc.branch; - } - if (frm.doc.designation) { - filters['designation'] = frm.doc.designation; - } + let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account', + 'currency', 'department', 'branch', 'designation']; + + fields.forEach(field => { + if (frm.doc[field]) { + filters[field] = frm.doc[field]; + } + }); + if (frm.doc.employees) { - filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + if (employees && employees.length) { + filters['employees'] = employees; + } } return filters; }, From 87b4e6ea323bf242e0661a8735c38d5cc5d4bea8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Jun 2021 23:27:24 +0530 Subject: [PATCH 293/550] fix: employee selection not working in payroll entry --- .../doctype/payroll_entry/payroll_entry.js | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index f2892600d1..496c37b2fa 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', { }); frm.set_query('employee', 'employees', () => { - if (!frm.doc.company) { - frappe.msgprint(__("Please set a Company")); - return []; + let error_fields = []; + let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date']; + + let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]); + + mandatory_fields.forEach(field => { + if (!frm.doc[field]) { + error_fields.push(frappe.unscrub(field)); + } + }); + + if (error_fields && error_fields.length) { + message = message + '

    • ' + error_fields.join('
    • ') + "
    "; + frappe.throw({ + message: message, + indicator: 'red', + title: __('Missing Fields') + }); } + return { query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", filters: frm.events.get_employee_filters(frm) @@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', { get_employee_filters: function (frm) { let filters = {}; - filters['company'] = frm.doc.company; - filters['start_date'] = frm.doc.start_date; - filters['end_date'] = frm.doc.end_date; filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet; - filters['payroll_frequency'] = frm.doc.payroll_frequency; - filters['payroll_payable_account'] = frm.doc.payroll_payable_account; - filters['currency'] = frm.doc.currency; - if (frm.doc.department) { - filters['department'] = frm.doc.department; - } - if (frm.doc.branch) { - filters['branch'] = frm.doc.branch; - } - if (frm.doc.designation) { - filters['designation'] = frm.doc.designation; - } + let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account', + 'currency', 'department', 'branch', 'designation']; + + fields.forEach(field => { + if (frm.doc[field]) { + filters[field] = frm.doc[field]; + } + }); + if (frm.doc.employees) { - filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + if (employees && employees.length) { + filters['employees'] = employees; + } } return filters; }, From f99f872946f178d76a823ac667927555fbdedf03 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 1 Jul 2021 11:50:48 +0530 Subject: [PATCH 294/550] fix: update cost not working in the draft bom --- erpnext/manufacturing/doctype/bom/bom.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 27019dbbae..15a7c316c9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -325,8 +325,7 @@ frappe.ui.form.on("BOM", { freeze: true, args: { update_parent: true, - from_child_bom:false, - save: frm.doc.docstatus === 1 ? true : false + from_child_bom:false }, callback: function(r) { refresh_field("items"); From eee03fcbabd1974ddbbefa12e1f5c34a128b371e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 1 Jul 2021 12:57:13 +0550 Subject: [PATCH 295/550] bumped to version 13.6.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 39d9a27615..0c96d325c2 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.5.2' +__version__ = '13.6.0' def get_default_company(user=None): '''Get default company for user''' From d45e307f02af919d6880a53d1e9e4bf406788ca7 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 1 Jul 2021 12:38:09 +0530 Subject: [PATCH 296/550] test: fix expected test failure (#26275) reference: https://github.com/frappe/frappe/pull/13557 --- .../test_patient_history_settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py index c93b788aed..33119d8185 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest import json -from frappe.utils import getdate +from frappe.utils import getdate, strip_html from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient class TestPatientHistorySettings(unittest.TestCase): @@ -44,9 +44,9 @@ class TestPatientHistorySettings(unittest.TestCase): self.assertTrue(medical_rec) medical_rec = frappe.get_doc("Patient Medical Record", medical_rec) - expected_subject = "Date: {0}
    Rating: 3
    Feedback: Test Patient History Settings
    ".format( + expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format( frappe.utils.format_date(getdate())) - self.assertEqual(medical_rec.subject, expected_subject) + self.assertEqual(strip_html(medical_rec.subject), expected_subject) self.assertEqual(medical_rec.patient, patient) self.assertEqual(medical_rec.communication_date, getdate()) @@ -101,4 +101,4 @@ def create_doc(patient): }).insert() doc.submit() - return doc \ No newline at end of file + return doc From d8bc51422656f6446584d7d7d75a7d0d209b7993 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Thu, 1 Jul 2021 14:06:01 +0530 Subject: [PATCH 297/550] fix: to fetch the correct field in Tax Rule (#25927) --- erpnext/accounts/doctype/tax_rule/tax_rule.js | 18 - .../accounts/doctype/tax_rule/tax_rule.json | 1273 +++-------------- .../doctype/tax_rule/test_tax_rule.py | 2 +- 3 files changed, 211 insertions(+), 1082 deletions(-) diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.js b/erpnext/accounts/doctype/tax_rule/tax_rule.js index 370890e4d8..bc497163e8 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.js +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.js @@ -1,24 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.add_fetch("customer", "customer_group", "customer_group" ); -cur_frm.add_fetch("supplier", "supplier_group_name", "supplier_group" ); - -frappe.ui.form.on("Tax Rule", "tax_type", function(frm) { - frm.toggle_reqd("sales_tax_template", frm.doc.tax_type=="Sales"); - frm.toggle_reqd("purchase_tax_template", frm.doc.tax_type=="Purchase"); -}) - -frappe.ui.form.on("Tax Rule", "onload", function(frm) { - if(frm.doc.__islocal) { - frm.set_value("use_for_shopping_cart", 1); - } -}) - -frappe.ui.form.on("Tax Rule", "refresh", function(frm) { - frappe.ui.form.trigger("Tax Rule", "tax_type"); -}) - frappe.ui.form.on("Tax Rule", "customer", function(frm) { if(frm.doc.customer) { frappe.call({ diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.json b/erpnext/accounts/doctype/tax_rule/tax_rule.json index ef155381c0..2746748432 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.json +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.json @@ -1,1103 +1,250 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "ACC-TAX-RULE-.YYYY.-.#####", - "beta": 0, - "creation": "2015-08-07 02:33:52.670866", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "ACC-TAX-RULE-.YYYY.-.#####", + "creation": "2015-08-07 02:33:52.670866", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "tax_type", + "use_for_shopping_cart", + "column_break_1", + "sales_tax_template", + "purchase_tax_template", + "filters", + "customer", + "supplier", + "item", + "billing_city", + "billing_county", + "billing_state", + "billing_zipcode", + "billing_country", + "tax_category", + "column_break_2", + "customer_group", + "supplier_group", + "item_group", + "shipping_city", + "shipping_county", + "shipping_state", + "shipping_zipcode", + "shipping_country", + "section_break_4", + "from_date", + "column_break_7", + "to_date", + "section_break_6", + "priority", + "column_break_20", + "company" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Sales", - "fieldname": "tax_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Tax Type", - "length": 0, - "no_copy": 0, - "options": "Sales\nPurchase", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Sales", + "fieldname": "tax_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Tax Type", + "options": "Sales\nPurchase" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "use_for_shopping_cart", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Use for Shopping Cart", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "1", + "fieldname": "use_for_shopping_cart", + "fieldtype": "Check", + "label": "Use for Shopping Cart" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Sales\"", - "fieldname": "sales_tax_template", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Tax Template", - "length": 0, - "no_copy": 0, - "options": "Sales Taxes and Charges Template", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.tax_type==\"Sales\"", + "fieldname": "sales_tax_template", + "fieldtype": "Link", + "label": "Sales Tax Template", + "options": "Sales Taxes and Charges Template" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Purchase\"", - "fieldname": "purchase_tax_template", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Tax Template", - "length": 0, - "no_copy": 0, - "options": "Purchase Taxes and Charges Template", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.tax_type==\"Purchase\"", + "fieldname": "purchase_tax_template", + "fieldtype": "Link", + "label": "Purchase Tax Template", + "options": "Purchase Taxes and Charges Template" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "filters", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Filters", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "filters", + "fieldtype": "Section Break", + "label": "Filters" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Sales\"", - "fieldname": "customer", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer", - "length": 0, - "no_copy": 0, - "options": "Customer", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.tax_type==\"Sales\"", + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Purchase\"", - "fieldname": "supplier", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplier", - "length": 0, - "no_copy": 0, - "options": "Supplier", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.tax_type==\"Purchase\"", + "fieldname": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item", + "fieldtype": "Link", + "label": "Item", + "options": "Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_city", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Billing City", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_city", + "fieldtype": "Data", + "label": "Billing City" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_county", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Billing County", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_county", + "fieldtype": "Data", + "label": "Billing County" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_state", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Billing State", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_state", + "fieldtype": "Data", + "label": "Billing State" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_zipcode", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Billing Zipcode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_zipcode", + "fieldtype": "Data", + "label": "Billing Zipcode" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "billing_country", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Billing Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "billing_country", + "fieldtype": "Link", + "label": "Billing Country", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tax_category", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Tax Category", - "length": 0, - "no_copy": 0, - "options": "Tax Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Sales\"", - "fieldname": "customer_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Group", - "length": 0, - "no_copy": 0, - "options": "Customer Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.tax_type==\"Sales\"", + "fetch_from": "customer.customer_group", + "fieldname": "customer_group", + "fieldtype": "Link", + "label": "Customer Group", + "options": "Customer Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.tax_type==\"Purchase\"", - "fieldname": "supplier_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Supplier Group", - "length": 0, - "no_copy": 0, - "options": "Supplier Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.tax_type==\"Purchase\"", + "fetch_from": "supplier.supplier_group", + "fieldname": "supplier_group", + "fieldtype": "Link", + "label": "Supplier Group", + "options": "Supplier Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_group", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Group", - "length": 0, - "no_copy": 0, - "options": "Item Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_city", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Shipping City", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_city", + "fieldtype": "Data", + "label": "Shipping City" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_county", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Shipping County", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_county", + "fieldtype": "Data", + "label": "Shipping County" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_state", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Shipping State", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_state", + "fieldtype": "Data", + "label": "Shipping State" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_zipcode", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Shipping Zipcode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_zipcode", + "fieldtype": "Data", + "label": "Shipping Zipcode" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_country", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Shipping Country", - "length": 0, - "no_copy": 0, - "options": "Country", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "shipping_country", + "fieldtype": "Link", + "label": "Shipping Country", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Validity", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Validity" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "priority", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Priority", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "1", + "fieldname": "priority", + "fieldtype": "Int", + "label": "Priority" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_20", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_20", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-12-27 01:22:17.721636", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Tax Rule", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2021-06-04 23:14:27.186879", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Rule", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py index ac1ffd9e75..cf7226822e 100644 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py @@ -50,7 +50,7 @@ class TestTaxRule(unittest.TestCase): tax_rule1 = make_tax_rule(customer_group= "All Customer Groups", sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") tax_rule1.save() - self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":0}), + self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}), "_Test Sales Taxes and Charges Template - _TC") def test_conflict_with_overlapping_dates(self): From 5a4251107c62cb96a4519585eb49c14fa091d038 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 1 Jul 2021 17:17:34 +0530 Subject: [PATCH 298/550] feat: Project Portal Enhancements (#26090) * fix: project portal enhancements * fix: timesheet table and task nesting * fix: semgrep and link issue * fix: sider * fix: project details view title * fix: project progress pills * fix: website route rule for project * fix: multi level nesting * fix: added subject and indentation Co-authored-by: Rucha Mahabal --- erpnext/hooks.py | 1 + .../includes/projects/project_row.html | 80 ++++--- .../includes/projects/project_tasks.html | 33 +-- .../includes/projects/project_timesheets.html | 54 +++-- erpnext/templates/pages/projects.html | 215 ++++++++++++------ erpnext/templates/pages/projects.py | 44 +--- 6 files changed, 250 insertions(+), 177 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 3da606b68b..ba10b58f85 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -158,6 +158,7 @@ website_route_rules = [ "parents": [{"label": _("Material Request"), "route": "material-requests"}] } }, + {"from_route": "/project", "to_route": "Project"} ] standard_portal_menu_items = [ diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html index 4c8c40db00..a256fbd677 100644 --- a/erpnext/templates/includes/projects/project_row.html +++ b/erpnext/templates/includes/projects/project_row.html @@ -1,28 +1,54 @@ -{% if doc.status=="Open" %} - +{% if doc.status == "Open" %} +
    +
    +
    + Link + {{ doc.name }} +
    +
    + {{ doc.project_name }} +
    +
    + {% if doc.percent_complete %} + {% set pill_class = "green" if doc.percent_complete | round == 100 else + "orange" %} +
    + + {{ frappe.utils.cint(doc.percent_complete) }} + % + +
    + {% else %} + + {{ doc.status }} + {% endif %} +
    + {% if doc["_assign"] %} + {% set assigned_users = json.loads(doc["_assign"])%} +
    + {% for user in assigned_users %} + {% set user_details = frappe + .db + .get_value("User", user, [ + "full_name", "user_image" + ], as_dict = True) %} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} + {% endfor %} +
    + {% endif %} +
    + {{ frappe.utils.pretty_date(doc.modified) }} +
    +
    +
    {% endif %} diff --git a/erpnext/templates/includes/projects/project_tasks.html b/erpnext/templates/includes/projects/project_tasks.html index 50b9f4b259..2b07a5f0d0 100644 --- a/erpnext/templates/includes/projects/project_tasks.html +++ b/erpnext/templates/includes/projects/project_tasks.html @@ -1,32 +1,5 @@ {% for task in doc.tasks %} - +
    + {{ task_row(task, 0) }} +
    {% endfor %} diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html index 05a07c12e8..fa5b2f9f2e 100644 --- a/erpnext/templates/includes/projects/project_timesheets.html +++ b/erpnext/templates/includes/projects/project_timesheets.html @@ -1,23 +1,33 @@ {% for timesheet in doc.timesheets %} - -{% endfor %} \ No newline at end of file +
    +
    +
    {{ timesheet.name }}
    + Link +
    {{ timesheet.status }}
    +
    {{ frappe.utils.format_date(timesheet.from_time, "medium") }}
    +
    {{ frappe.utils.format_date(timesheet.to_time, "medium") }}
    +
    + {% set user_details = frappe + .db + .get_value("User", timesheet.modified_by, [ + "full_name", "user_image" + ], as_dict = True) + %} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} +
    +
    + {{ frappe.utils.pretty_date(timesheet.modified) }} +
    +
    +
    +{% endfor %} diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index 7e294e076b..76eaf75cf3 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -1,90 +1,173 @@ {% extends "templates/web.html" %} -{% block title %}{{ doc.project_name }}{% endblock %} +{% block title %} + {{ doc.project_name }} +{% endblock %} + +{% block head_include %} + +{% endblock %} {% block header %} -

    {{ doc.project_name }}

    +

    {{ doc.project_name }}

    {% endblock %} {% block style %} - + {% endblock %} - {% block page_content %} -{% if doc.percent_complete %} -
    -
    -
    -
    -{% endif %} -
    -

    {{ _("Tasks") }}

    - {{ _("New task") }} -
    + {{ progress_bar(doc.percent_complete) }} -

    - -

    +
    +

    Status:

    +

    Progress: + {{ doc.percent_complete }} + % +

    +

    Hours Spent: + {{ doc.actual_time }} +

    +
    -{% if doc.tasks %} -
    -
    - {% include "erpnext/templates/includes/projects/project_tasks.html" %} -
    -

    -

    -{% else %} -

    {{ _("No tasks") }}

    -{% endif %} + {{ progress_bar(doc.percent_complete) }} + {% if doc.tasks %} +
    +
    +
    +
    +

    Tasks

    +

    Status

    +

    End Date

    +

    Assigned To

    + +
    +
    + {% include "erpnext/templates/includes/projects/project_tasks.html" %} +
    +
    + {% else %} +

    {{ _("No Tasks") }}

    + {% endif %} -
    + {% if doc.timesheets %} +
    +
    +
    +
    +

    Timesheets

    +

    Status

    +

    From

    +

    To

    +

    Modified By

    +

    Modified On

    +
    +
    + {% include "erpnext/templates/includes/projects/project_timesheets.html" %} +
    +
    + {% else %} +

    {{ _("No Timesheets") }}

    + {% endif %} -

    {{ _("Timesheets") }}

    + {% if doc.attachments %} +
    -{% if doc.timesheets %} -
    - {% include "erpnext/templates/includes/projects/project_timesheets.html" %} -
    - {% if doc.timesheets|length > 9 %} -

    {{ _("More") }}

    - {% endif %} -{% else %} -

    {{ _("No time sheets") }}

    -{% endif %} - -{% if doc.attachments %} -
    - -

    {{ _("Attachments") }}

    -
    - {% for attachment in doc.attachments %} - - {% endfor %} -
    -{% endif %} +

    {{ _("Attachments") }}

    +
    + {% for attachment in doc.attachments %} + + {% endfor %} +
    + {% endif %}
    {% endblock %} + +{% macro progress_bar(percent_complete) %} +{% if percent_complete %} +
    +
    +
    +{% else %} +
    +{% endif %} +{% endmacro %} + +{% macro task_row(task, indent) %} +
    + +
    {{ task.status }}
    +
    + {% if task.exp_end_date %} + {{ task.exp_end_date }} + {% else %} + -- + {% endif %} +
    +
    + {% if task["_assign"] %} + {% set assigned_users = json.loads(task["_assign"])%} + {% for user in assigned_users %} + {% set user_details = frappe.db.get_value("User", user, + ["full_name", "user_image"], + as_dict = True)%} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} + {% endfor %} + {% endif %} +
    +
    + {{ frappe.utils.pretty_date(task.modified) }} +
    +
    +{% if task.children %} + {% for child in task.children %} + {{ task_row(child, indent + 30) }} + {% endfor %} +{% endif %} +{% endmacro %} diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index d23fed9e7d..7ff495402a 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -32,29 +32,17 @@ def get_tasks(project, start=0, search=None, item_status=None): filters = {"project": project} if search: filters["subject"] = ("like", "%{0}%".format(search)) - # if item_status: -# filters["status"] = item_status tasks = frappe.get_all("Task", filters=filters, - fields=["name", "subject", "status", "_seen", "_comments", "modified", "description"], + fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"], limit_start=start, limit_page_length=10) - + task_nest = [] for task in tasks: - task.todo = frappe.get_all('ToDo',filters={'reference_name':task.name, 'reference_type':'Task'}, - fields=["assigned_by", "owner", "modified", "modified_by"]) - - if task.todo: - task.todo=task.todo[0] - task.todo.user_image = frappe.db.get_value('User', task.todo.owner, 'user_image') - - - task.comment_count = len(json.loads(task._comments or "[]")) - - task.css_seen = '' - if task._seen: - if frappe.session.user in json.loads(task._seen): - task.css_seen = 'seen' - - return tasks + if task.is_group: + child_tasks = list(filter(lambda x: x.parent_task == task.name, tasks)) + if len(child_tasks): + task.children = child_tasks + task_nest.append(task) + return list(filter(lambda x: not x.parent_task, tasks)) @frappe.whitelist() def get_task_html(project, start=0, item_status=None): @@ -74,19 +62,11 @@ def get_timesheets(project, start=0, search=None): fields=['project','activity_type','from_time','to_time','parent'], limit_start=start, limit_page_length=10) for timesheet in timesheets: - timesheet.infos = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, - fields=['name','_comments','_seen','status','modified','modified_by'], + info = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, + fields=['name','status','modified','modified_by'], limit_start=start, limit_page_length=10) - - for timesheet.info in timesheet.infos: - timesheet.info.user_image = frappe.db.get_value('User', timesheet.info.modified_by, 'user_image') - - timesheet.info.comment_count = len(json.loads(timesheet.info._comments or "[]")) - - timesheet.info.css_seen = '' - if timesheet.info._seen: - if frappe.session.user in json.loads(timesheet.info._seen): - timesheet.info.css_seen = 'seen' + if len(info): + timesheet.update(info[0]) return timesheets @frappe.whitelist() From 752f099e9dd5187669d11b6420dece399d072aff Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Jul 2021 17:20:24 +0530 Subject: [PATCH 299/550] fix: Order Items by weightage in the web items query --- erpnext/shopping_cart/product_query.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index d96d803416..6c92d967d0 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -71,7 +71,8 @@ class ProductQuery: ], or_filters=self.or_filters, start=start, - limit=self.page_length + limit=self.page_length, + order_by="weightage desc" ) items_dict = {item.name: item for item in items} @@ -86,7 +87,8 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, start=start, - limit=self.page_length + limit=self.page_length, + order_by="weightage desc" ) # Combine results having context of website item groups into item results From ba2c3c776f15394ed287454dd9ccce60acd6f228 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 18:56:51 +0530 Subject: [PATCH 300/550] fix: Bank statement import --- .../doctype/bank_statement_import/bank_statement_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 5f110e2727..ffc9d1c465 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -51,7 +51,7 @@ class BankStatementImport(DataImport): self.import_file, self.google_sheets_url ) - if 'Bank Account' not in json.dumps(preview): + if 'Bank Account' not in json.dumps(preview['columns']): frappe.throw(_("Please add the Bank Account column")) from frappe.core.page.background_jobs.background_jobs import get_info From 81522ec521333a078775865fd31ddf008bbfebed Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 1 Jul 2021 19:34:58 +0530 Subject: [PATCH 301/550] fix: validate Product Bundle for existing transactions before deletion (#25977) From d2c86bb9d7ba07fc306e2b9b5d14c6b769322b6b Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Thu, 1 Jul 2021 19:59:08 +0530 Subject: [PATCH 302/550] fix: Fixed Budget Variance Graph color from all black to default --- .../report/budget_variance_report/budget_variance_report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 9c9ada871c..f1b231b690 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data): {'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} ] - } + }, + 'type' : 'bar' } From 74b8c99bc29eb2ffe52ee06bff829cc06ad673c8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 15 Apr 2021 11:30:55 +0530 Subject: [PATCH 303/550] feat: Introduced cypress tests in erpnext --- .eslintrc | 3 + cypress.json | 11 + cypress/fixtures/example.json | 5 + cypress/integration/test_customer.js | 13 ++ cypress/plugins/index.js | 17 ++ cypress/support/commands.js | 326 +++++++++++++++++++++++++++ cypress/support/index.js | 25 ++ cypress/tsconfig.json | 12 + 8 files changed, 412 insertions(+) create mode 100644 cypress.json create mode 100644 cypress/fixtures/example.json create mode 100644 cypress/integration/test_customer.js create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/commands.js create mode 100644 cypress/support/index.js create mode 100644 cypress/tsconfig.json diff --git a/.eslintrc b/.eslintrc index e40502acd6..a5fcc1bcba 100644 --- a/.eslintrc +++ b/.eslintrc @@ -147,11 +147,14 @@ "Chart": true, "Cypress": true, "cy": true, + "describe": true, + "expect": true, "it": true, "context": true, "before": true, "beforeEach": true, "onScan": true, "extend_cscript": true + "localforage": true, } } diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000000..f7bd9d9a17 --- /dev/null +++ b/cypress.json @@ -0,0 +1,11 @@ +{ + "baseUrl": "http://test-develop:8001/", + "projectId": "92odwv", + "adminPassword": "admin", + "defaultCommandTimeout": 20000, + "pageLoadTimeout": 15000, + "retries": { + "runMode": 2, + "openMode": 2 + } +} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json new file mode 100644 index 0000000000..da18d9352a --- /dev/null +++ b/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js new file mode 100644 index 0000000000..3d6ed5d0d8 --- /dev/null +++ b/cypress/integration/test_customer.js @@ -0,0 +1,13 @@ + +context('Customer', () => { + before(() => { + cy.login(); + }); + it('Check Customer Group', () => { + cy.visit(`app/customer/`); + cy.get('.primary-action').click(); + cy.wait(500); + cy.get('.custom-actions > .btn').click(); + cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups'); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000000..07d9804a73 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = () => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000000..1964b96d70 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,326 @@ +import 'cypress-file-upload'; +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }); +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); +Cypress.Commands.add('login', (email, password) => { + if (!email) { + email = 'Administrator'; + } + if (!password) { + password = Cypress.config('adminPassword'); + } + cy.request({ + url: '/api/method/login', + method: 'POST', + body: { + usr: email, + pwd: password + } + }); +}); + +Cypress.Commands.add('call', (method, args) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + url: `/api/method/${method}`, + method: 'POST', + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('get_list', (doctype, fields = [], filters = []) => { + filters = JSON.stringify(filters); + fields = JSON.stringify(fields); + let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`; + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'GET', + url, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('get_doc', (doctype, name) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'GET', + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body; + }); + }); +}); + +Cypress.Commands.add('remove_doc', (doctype, name) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'DELETE', + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + } + }) + .then(res => { + expect(res.status).eq(202); + return res.body; + }); + }); +}); + +Cypress.Commands.add('create_records', doc => { + return cy + .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc}) + .then(r => r.message); +}); + +Cypress.Commands.add('set_value', (doctype, name, obj) => { + return cy.call('frappe.client.set_value', { + doctype, + name, + fieldname: obj + }); +}); + +Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { + cy.get_field(fieldname, fieldtype).as('input'); + + if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { + cy.get('@input').click().wait(200); + cy.get('.datepickers-container .datepicker.active').should('exist'); + } + if (fieldtype === 'Time') { + cy.get('@input').clear().wait(200); + } + + if (fieldtype === 'Select') { + cy.get('@input').select(value); + } else { + cy.get('@input').type(value, {waitForAnimations: false, force: true}); + } + return cy.get('@input'); +}); + +Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { + let selector = `.form-control[data-fieldname="${fieldname}"]`; + + if (fieldtype === 'Text Editor') { + selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } + if (fieldtype === 'Code') { + selector = `[data-fieldname="${fieldname}"] .ace_text-input`; + } + + return cy.get(selector); +}); + +Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => { + cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input'); + + if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { + cy.get('@input').click().wait(200); + cy.get('.datepickers-container .datepicker.active').should('exist'); + } + if (fieldtype === 'Time') { + cy.get('@input').clear().wait(200); + } + + if (fieldtype === 'Select') { + cy.get('@input').select(value); + } else { + cy.get('@input').type(value, {waitForAnimations: false, force: true}); + } + return cy.get('@input'); +}); + +Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => { + let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`; + selector += ` [data-idx="${row_idx}"]`; + selector += ` .form-in-grid`; + + if (fieldtype === 'Text Editor') { + selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } else if (fieldtype === 'Code') { + selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; + } else { + selector += ` .form-control[data-fieldname="${fieldname}"]`; + } + + return cy.get(selector); +}); + +Cypress.Commands.add('awesomebar', text => { + cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100}); +}); + +Cypress.Commands.add('new_form', doctype => { + let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); + cy.visit(`/app/${dt_in_route}/new`); + cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`); + cy.get('body').should('have.attr', 'data-ajax-state', 'complete'); +}); + +Cypress.Commands.add('go_to_list', doctype => { + cy.visit(`/app/list/${doctype}/list`); +}); + +Cypress.Commands.add('clear_cache', () => { + cy.window() + .its('frappe') + .then(frappe => { + frappe.ui.toolbar.clear_cache(); + }); +}); + +Cypress.Commands.add('dialog', opts => { + return cy.window().then(win => { + var d = new win.frappe.ui.Dialog(opts); + d.show(); + return d; + }); +}); + +Cypress.Commands.add('get_open_dialog', () => { + return cy.get('.modal:visible').last(); +}); + +Cypress.Commands.add('hide_dialog', () => { + cy.wait(300); + cy.get_open_dialog().find('.btn-modal-close').click(); + cy.get('.modal:visible').should('not.exist'); +}); + +Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { + return cy + .window() + .its('frappe.csrf_token') + .then(csrf_token => { + return cy + .request({ + method: 'POST', + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': csrf_token + }, + failOnStatusCode: !ignore_duplicate + }) + .then(res => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + expect(res.status).to.be.oneOf(status_codes); + return res.body.data; + }); + }); +}); + +Cypress.Commands.add('add_filter', () => { + cy.get('.filter-section .filter-button').click(); + cy.wait(300); + cy.get('.filter-popover').should('exist'); +}); + +Cypress.Commands.add('clear_filters', () => { + cy.get('.filter-section .filter-button').click(); + cy.wait(300); + cy.get('.filter-popover').should('exist'); + cy.get('.filter-popover').find('.clear-filters').click(); + cy.get('.filter-section .filter-button').click(); + cy.window().its('cur_list').then(cur_list => { + cur_list && cur_list.filter_area && cur_list.filter_area.clear(); + }); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 0000000000..1bee72d2ca --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,25 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +Cypress.Cookies.defaults({ + preserve: 'sid' +}); \ No newline at end of file diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000000..d90ebf6856 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": "../node_modules", + "types": [ + "cypress" + ] + }, + "include": [ + "**/*.*" + ] +} \ No newline at end of file From 3f14b92e2cd60a32e51c86a79e7497315cf2530b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 4 Jun 2021 16:41:32 +0530 Subject: [PATCH 304/550] ci: UI tests workflow --- .eslintrc | 2 +- .github/workflows/ui-tests.yml | 101 +++++++++++++++++++++++++++++++++ cypress.json | 2 +- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ui-tests.yml diff --git a/.eslintrc b/.eslintrc index a5fcc1bcba..cb45ce5f69 100644 --- a/.eslintrc +++ b/.eslintrc @@ -154,7 +154,7 @@ "before": true, "beforeEach": true, "onScan": true, - "extend_cscript": true + "extend_cscript": true, "localforage": true, } } diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 0000000000..b187dff5c5 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,101 @@ +name: UI + +on: + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-18.04 + + strategy: + fail-fast: false + + name: UI Tests (Cypress) + + services: + mysql: + image: mariadb:10.3 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - uses: actions/setup-node@v2 + with: + node-version: 14 + check-latest: true + + - name: Add to Hosts + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Cache cypress binary + uses: actions/cache@v2 + with: + path: ~/.cache + key: ${{ runner.os }}-cypress- + restore-keys: | + ${{ runner.os }}-cypress- + ${{ runner.os }}- + + - name: Install + run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + env: + DB: mariadb + TYPE: ui + + - name: Site Setup + run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests + + + - name: Build Assets + run: cd ~/frappe-bench/ && bench build + + - name: UI Tests + run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} diff --git a/cypress.json b/cypress.json index f7bd9d9a17..839bb08fa9 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,5 @@ { - "baseUrl": "http://test-develop:8001/", + "baseUrl": "http://test_site:8000/", "projectId": "92odwv", "adminPassword": "admin", "defaultCommandTimeout": 20000, From 4d9c08d92ac0bfae1634f8a5058dda3d47250fc9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 29 Jun 2021 21:05:43 +0530 Subject: [PATCH 305/550] chore: add project id for cypress --- cypress.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress.json b/cypress.json index 839bb08fa9..02b10d893f 100644 --- a/cypress.json +++ b/cypress.json @@ -1,6 +1,6 @@ { "baseUrl": "http://test_site:8000/", - "projectId": "92odwv", + "projectId": "da59y9", "adminPassword": "admin", "defaultCommandTimeout": 20000, "pageLoadTimeout": 15000, From a68344fe8a1e068eea910c70d4f13edf84e1f715 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 29 Jun 2021 21:33:55 +0530 Subject: [PATCH 306/550] refactor: extend commands from frappe --- .github/workflows/ui-tests.yml | 3 + cypress/support/commands.js | 301 --------------------------------- cypress/support/index.js | 3 +- 3 files changed, 5 insertions(+), 302 deletions(-) diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index b187dff5c5..4bc55da1d8 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -91,6 +91,9 @@ jobs: - name: Site Setup run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests + - name: cypress pre-requisites + run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile + - name: Build Assets run: cd ~/frappe-bench/ && bench build diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 1964b96d70..7929a2e0ef 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,4 +1,3 @@ -import 'cypress-file-upload'; // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite @@ -24,303 +23,3 @@ import 'cypress-file-upload'; // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); -Cypress.Commands.add('login', (email, password) => { - if (!email) { - email = 'Administrator'; - } - if (!password) { - password = Cypress.config('adminPassword'); - } - cy.request({ - url: '/api/method/login', - method: 'POST', - body: { - usr: email, - pwd: password - } - }); -}); - -Cypress.Commands.add('call', (method, args) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - url: `/api/method/${method}`, - method: 'POST', - body: args, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(200); - return res.body; - }); - }); -}); - -Cypress.Commands.add('get_list', (doctype, fields = [], filters = []) => { - filters = JSON.stringify(filters); - fields = JSON.stringify(fields); - let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`; - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'GET', - url, - headers: { - Accept: 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(200); - return res.body; - }); - }); -}); - -Cypress.Commands.add('get_doc', (doctype, name) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'GET', - url: `/api/resource/${doctype}/${name}`, - headers: { - Accept: 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(200); - return res.body; - }); - }); -}); - -Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'POST', - url: `/api/resource/${doctype}`, - body: args, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - failOnStatusCode: !ignore_duplicate - }) - .then(res => { - let status_codes = [200]; - if (ignore_duplicate) { - status_codes.push(409); - } - expect(res.status).to.be.oneOf(status_codes); - return res.body; - }); - }); -}); - -Cypress.Commands.add('remove_doc', (doctype, name) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'DELETE', - url: `/api/resource/${doctype}/${name}`, - headers: { - Accept: 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - } - }) - .then(res => { - expect(res.status).eq(202); - return res.body; - }); - }); -}); - -Cypress.Commands.add('create_records', doc => { - return cy - .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc}) - .then(r => r.message); -}); - -Cypress.Commands.add('set_value', (doctype, name, obj) => { - return cy.call('frappe.client.set_value', { - doctype, - name, - fieldname: obj - }); -}); - -Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { - cy.get_field(fieldname, fieldtype).as('input'); - - if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { - cy.get('@input').click().wait(200); - cy.get('.datepickers-container .datepicker.active').should('exist'); - } - if (fieldtype === 'Time') { - cy.get('@input').clear().wait(200); - } - - if (fieldtype === 'Select') { - cy.get('@input').select(value); - } else { - cy.get('@input').type(value, {waitForAnimations: false, force: true}); - } - return cy.get('@input'); -}); - -Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { - let selector = `.form-control[data-fieldname="${fieldname}"]`; - - if (fieldtype === 'Text Editor') { - selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; - } - if (fieldtype === 'Code') { - selector = `[data-fieldname="${fieldname}"] .ace_text-input`; - } - - return cy.get(selector); -}); - -Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => { - cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input'); - - if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { - cy.get('@input').click().wait(200); - cy.get('.datepickers-container .datepicker.active').should('exist'); - } - if (fieldtype === 'Time') { - cy.get('@input').clear().wait(200); - } - - if (fieldtype === 'Select') { - cy.get('@input').select(value); - } else { - cy.get('@input').type(value, {waitForAnimations: false, force: true}); - } - return cy.get('@input'); -}); - -Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => { - let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`; - selector += ` [data-idx="${row_idx}"]`; - selector += ` .form-in-grid`; - - if (fieldtype === 'Text Editor') { - selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; - } else if (fieldtype === 'Code') { - selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; - } else { - selector += ` .form-control[data-fieldname="${fieldname}"]`; - } - - return cy.get(selector); -}); - -Cypress.Commands.add('awesomebar', text => { - cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100}); -}); - -Cypress.Commands.add('new_form', doctype => { - let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); - cy.visit(`/app/${dt_in_route}/new`); - cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`); - cy.get('body').should('have.attr', 'data-ajax-state', 'complete'); -}); - -Cypress.Commands.add('go_to_list', doctype => { - cy.visit(`/app/list/${doctype}/list`); -}); - -Cypress.Commands.add('clear_cache', () => { - cy.window() - .its('frappe') - .then(frappe => { - frappe.ui.toolbar.clear_cache(); - }); -}); - -Cypress.Commands.add('dialog', opts => { - return cy.window().then(win => { - var d = new win.frappe.ui.Dialog(opts); - d.show(); - return d; - }); -}); - -Cypress.Commands.add('get_open_dialog', () => { - return cy.get('.modal:visible').last(); -}); - -Cypress.Commands.add('hide_dialog', () => { - cy.wait(300); - cy.get_open_dialog().find('.btn-modal-close').click(); - cy.get('.modal:visible').should('not.exist'); -}); - -Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => { - return cy - .window() - .its('frappe.csrf_token') - .then(csrf_token => { - return cy - .request({ - method: 'POST', - url: `/api/resource/${doctype}`, - body: args, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - failOnStatusCode: !ignore_duplicate - }) - .then(res => { - let status_codes = [200]; - if (ignore_duplicate) { - status_codes.push(409); - } - expect(res.status).to.be.oneOf(status_codes); - return res.body.data; - }); - }); -}); - -Cypress.Commands.add('add_filter', () => { - cy.get('.filter-section .filter-button').click(); - cy.wait(300); - cy.get('.filter-popover').should('exist'); -}); - -Cypress.Commands.add('clear_filters', () => { - cy.get('.filter-section .filter-button').click(); - cy.wait(300); - cy.get('.filter-popover').should('exist'); - cy.get('.filter-popover').find('.clear-filters').click(); - cy.get('.filter-section .filter-button').click(); - cy.window().its('cur_list').then(cur_list => { - cur_list && cur_list.filter_area && cur_list.filter_area.clear(); - }); -}); diff --git a/cypress/support/index.js b/cypress/support/index.js index 1bee72d2ca..72070cc81c 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -15,6 +15,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import '../../../frappe/cypress/support/commands' // eslint-disable-line // Alternatively you can use CommonJS syntax: @@ -22,4 +23,4 @@ import './commands'; Cypress.Cookies.defaults({ preserve: 'sid' -}); \ No newline at end of file +}); From 8ebf32e18f7f8eb9fda17ef7b0d35d7ea974d0b4 Mon Sep 17 00:00:00 2001 From: Ankush Date: Fri, 2 Jul 2021 11:09:19 +0530 Subject: [PATCH 307/550] fix: undo changes to issue.py (#26291) The fix ported from v13 to develop is not valid because of the new generic SLA feature. Hard resetting the file to the previous version. ec25d5938b2170e557a08991f891e945925943f3 --- erpnext/support/doctype/issue/issue.py | 105 +------------------------ 1 file changed, 1 insertion(+), 104 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index e092b07222..dd6d647abc 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -26,9 +26,6 @@ class Issue(Document): self.set_lead_contact(self.raised_by) - if not self.service_level_agreement: - self.reset_sla_fields() - def on_update(self): # Add a communication in the issue timeline if self.flags.create_communication and self.via_customer_portal: @@ -54,106 +51,6 @@ class Issue(Document): self.company = frappe.db.get_value("Lead", self.lead, "company") or \ frappe.db.get_default("Company") - def reset_sla_fields(self): - self.agreement_status = "" - self.response_by = "" - self.resolution_by = "" - self.response_by_variance = 0 - self.resolution_by_variance = 0 - - def update_status(self): - status = frappe.db.get_value("Issue", self.name, "status") - if self.status != "Open" and status == "Open" and not self.first_responded_on: - self.first_responded_on = frappe.flags.current_time or now_datetime() - - if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: - self.resolution_date = frappe.flags.current_time or now_datetime() - if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing": - set_service_level_agreement_variance(issue=self.name) - self.update_agreement_status() - set_resolution_time(issue=self) - set_user_resolution_time(issue=self) - - if self.status == "Open" and status != "Open": - # if no date, it should be set as None and not a blank string "", as per mysql strict config - self.resolution_date = None - self.reset_issue_metrics() - # enable SLA and variance on Reopen - self.agreement_status = "Ongoing" - set_service_level_agreement_variance(issue=self.name) - - self.handle_hold_time(status) - - def handle_hold_time(self, status): - if self.service_level_agreement: - # set response and resolution variance as None as the issue is on Hold - pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"], - filters={"parent": self.service_level_agreement}) - hold_statuses = [entry.status for entry in pause_sla_on] - update_values = {} - - if hold_statuses: - if self.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() - if not self.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 - - # calculate hold time when status is changed from any hold status to any non-hold status - if self.status not in hold_statuses and status in hold_statuses: - hold_time = self.total_hold_time if self.total_hold_time else 0 - now_time = frappe.flags.current_time or now_datetime() - last_hold_time = 0 - if self.on_hold_since: - # last_hold_time will be added to the sla variables - last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time - - # re-calculate SLA variables after issue changes from any hold status to any non-hold status - # add hold time to SLA variables - start_date_time = get_datetime(self.service_level_agreement_creation) - priority = get_priority(self) - now_time = frappe.flags.current_time or now_datetime() - - if not self.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time - - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - update_values['on_hold_since'] = None - - self.db_set(update_values) - - def update_agreement_status(self): - if self.service_level_agreement and self.agreement_status == "Ongoing": - if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0: - - self.agreement_status = "Failed" - else: - self.agreement_status = "Fulfilled" - - def update_agreement_status_on_custom_status(self): - """ - Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status - """ - if not self.first_responded_on: # first_responded_on set when first reply is sent to customer - self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2) - - if not self.resolution_date: # resolution_date set when issue has been closed - self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2) - - self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" - def create_communication(self): communication = frappe.new_doc("Communication") communication.update({ @@ -318,4 +215,4 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals def get_holidays(holiday_list_name): holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) holidays = [holiday.holiday_date for holiday in holiday_list.holidays] - return holidays + return holidays \ No newline at end of file From 5173e74a041d33d87d4ab3172a4bfc4b64d66381 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 2 Jul 2021 11:48:46 +0530 Subject: [PATCH 308/550] fix: Project Portal Enhancements (#26290) * fix: project portal enhancements * fix: condition for pills --- erpnext/hooks.py | 1 + .../includes/projects/project_row.html | 80 ++++--- .../includes/projects/project_tasks.html | 33 +-- .../includes/projects/project_timesheets.html | 54 +++-- erpnext/templates/pages/projects.html | 215 ++++++++++++------ erpnext/templates/pages/projects.py | 41 +--- 6 files changed, 250 insertions(+), 174 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 8ad77a1524..52daec9180 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -157,6 +157,7 @@ website_route_rules = [ "parents": [{"label": _("Material Request"), "route": "material-requests"}] } }, + {"from_route": "/project", "to_route": "Project"} ] standard_portal_menu_items = [ diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html index 4c8c40db00..bfd63494c3 100644 --- a/erpnext/templates/includes/projects/project_row.html +++ b/erpnext/templates/includes/projects/project_row.html @@ -1,28 +1,54 @@ -{% if doc.status=="Open" %} - +{% if doc.status == "Open" %} +
    +
    +
    + Link + {{ doc.name }} +
    +
    + {{ doc.project_name }} +
    +
    + {% if doc.percent_complete %} + {% set pill_class = "green" if doc.percent_complete | round == 100 else + "orange" %} +
    + + {{ frappe.utils.cint(doc.percent_complete) }} + % + +
    + {% else %} + + {{ doc.status }} + {% endif %} +
    + {% if doc["_assign"] %} + {% set assigned_users = json.loads(doc["_assign"])%} +
    + {% for user in assigned_users %} + {% set user_details = frappe + .db + .get_value("User", user, [ + "full_name", "user_image" + ], as_dict = True) %} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} + {% endfor %} +
    + {% endif %} +
    + {{ frappe.utils.pretty_date(doc.modified) }} +
    +
    +
    {% endif %} diff --git a/erpnext/templates/includes/projects/project_tasks.html b/erpnext/templates/includes/projects/project_tasks.html index 50b9f4b259..2b07a5f0d0 100644 --- a/erpnext/templates/includes/projects/project_tasks.html +++ b/erpnext/templates/includes/projects/project_tasks.html @@ -1,32 +1,5 @@ {% for task in doc.tasks %} - +
    + {{ task_row(task, 0) }} +
    {% endfor %} diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html index 05a07c12e8..850c5e9863 100644 --- a/erpnext/templates/includes/projects/project_timesheets.html +++ b/erpnext/templates/includes/projects/project_timesheets.html @@ -1,23 +1,33 @@ {% for timesheet in doc.timesheets %} - -{% endfor %} \ No newline at end of file +
    +
    +
    {{ timesheet.name }}
    + Link +
    {{ timesheet.status }}
    +
    {{ frappe.utils.format_date(timesheet.from_time, "medium") }}
    +
    {{ frappe.utils.format_date(timesheet.to_time, "medium") }}
    +
    + {% set user_details = frappe + .db + .get_value("User", timesheet.modified_by, [ + "full_name", "user_image" + ], as_dict = True) + %} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} +
    +
    + {{ frappe.utils.pretty_date(timesheet.modified) }} +
    +
    +
    +{% endfor %} diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html index 7e294e076b..76eaf75cf3 100644 --- a/erpnext/templates/pages/projects.html +++ b/erpnext/templates/pages/projects.html @@ -1,90 +1,173 @@ {% extends "templates/web.html" %} -{% block title %}{{ doc.project_name }}{% endblock %} +{% block title %} + {{ doc.project_name }} +{% endblock %} + +{% block head_include %} + +{% endblock %} {% block header %} -

    {{ doc.project_name }}

    +

    {{ doc.project_name }}

    {% endblock %} {% block style %} - + {% endblock %} - {% block page_content %} -{% if doc.percent_complete %} -
    -
    -
    -
    -{% endif %} -
    -

    {{ _("Tasks") }}

    - {{ _("New task") }} -
    + {{ progress_bar(doc.percent_complete) }} -

    - -

    +
    +

    Status:

    +

    Progress: + {{ doc.percent_complete }} + % +

    +

    Hours Spent: + {{ doc.actual_time }} +

    +
    -{% if doc.tasks %} -
    -
    - {% include "erpnext/templates/includes/projects/project_tasks.html" %} -
    -

    -

    -{% else %} -

    {{ _("No tasks") }}

    -{% endif %} + {{ progress_bar(doc.percent_complete) }} + {% if doc.tasks %} +
    +
    +
    +
    +

    Tasks

    +

    Status

    +

    End Date

    +

    Assigned To

    + +
    +
    + {% include "erpnext/templates/includes/projects/project_tasks.html" %} +
    +
    + {% else %} +

    {{ _("No Tasks") }}

    + {% endif %} -
    + {% if doc.timesheets %} +
    +
    +
    +
    +

    Timesheets

    +

    Status

    +

    From

    +

    To

    +

    Modified By

    +

    Modified On

    +
    +
    + {% include "erpnext/templates/includes/projects/project_timesheets.html" %} +
    +
    + {% else %} +

    {{ _("No Timesheets") }}

    + {% endif %} -

    {{ _("Timesheets") }}

    + {% if doc.attachments %} +
    -{% if doc.timesheets %} -
    - {% include "erpnext/templates/includes/projects/project_timesheets.html" %} -
    - {% if doc.timesheets|length > 9 %} -

    {{ _("More") }}

    - {% endif %} -{% else %} -

    {{ _("No time sheets") }}

    -{% endif %} - -{% if doc.attachments %} -
    - -

    {{ _("Attachments") }}

    -
    - {% for attachment in doc.attachments %} - - {% endfor %} -
    -{% endif %} +

    {{ _("Attachments") }}

    +
    + {% for attachment in doc.attachments %} + + {% endfor %} +
    + {% endif %}
    {% endblock %} + +{% macro progress_bar(percent_complete) %} +{% if percent_complete %} +
    +
    +
    +{% else %} +
    +{% endif %} +{% endmacro %} + +{% macro task_row(task, indent) %} +
    + +
    {{ task.status }}
    +
    + {% if task.exp_end_date %} + {{ task.exp_end_date }} + {% else %} + -- + {% endif %} +
    +
    + {% if task["_assign"] %} + {% set assigned_users = json.loads(task["_assign"])%} + {% for user in assigned_users %} + {% set user_details = frappe.db.get_value("User", user, + ["full_name", "user_image"], + as_dict = True)%} + {% if user_details.user_image %} + + + + {% else %} + +
    + {{ frappe.utils.get_abbr(user_details.full_name) }} +
    +
    + {% endif %} + {% endfor %} + {% endif %} +
    +
    + {{ frappe.utils.pretty_date(task.modified) }} +
    +
    +{% if task.children %} + {% for child in task.children %} + {{ task_row(child, indent + 30) }} + {% endfor %} +{% endif %} +{% endmacro %} diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index d23fed9e7d..b369cb6a99 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -35,26 +35,16 @@ def get_tasks(project, start=0, search=None, item_status=None): # if item_status: # filters["status"] = item_status tasks = frappe.get_all("Task", filters=filters, - fields=["name", "subject", "status", "_seen", "_comments", "modified", "description"], + fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"], limit_start=start, limit_page_length=10) - + task_nest = [] for task in tasks: - task.todo = frappe.get_all('ToDo',filters={'reference_name':task.name, 'reference_type':'Task'}, - fields=["assigned_by", "owner", "modified", "modified_by"]) - - if task.todo: - task.todo=task.todo[0] - task.todo.user_image = frappe.db.get_value('User', task.todo.owner, 'user_image') - - - task.comment_count = len(json.loads(task._comments or "[]")) - - task.css_seen = '' - if task._seen: - if frappe.session.user in json.loads(task._seen): - task.css_seen = 'seen' - - return tasks + if task.is_group: + child_tasks = list(filter(lambda x: x.parent_task == task.name, tasks)) + if len(child_tasks): + task.children = child_tasks + task_nest.append(task) + return list(filter(lambda x: not x.parent_task, tasks)) @frappe.whitelist() def get_task_html(project, start=0, item_status=None): @@ -74,19 +64,12 @@ def get_timesheets(project, start=0, search=None): fields=['project','activity_type','from_time','to_time','parent'], limit_start=start, limit_page_length=10) for timesheet in timesheets: - timesheet.infos = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, - fields=['name','_comments','_seen','status','modified','modified_by'], + info = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, + fields=['name','status','modified','modified_by'], limit_start=start, limit_page_length=10) - for timesheet.info in timesheet.infos: - timesheet.info.user_image = frappe.db.get_value('User', timesheet.info.modified_by, 'user_image') - - timesheet.info.comment_count = len(json.loads(timesheet.info._comments or "[]")) - - timesheet.info.css_seen = '' - if timesheet.info._seen: - if frappe.session.user in json.loads(timesheet.info._seen): - timesheet.info.css_seen = 'seen' + if len(info): + timesheet.update(info[0]) return timesheets @frappe.whitelist() From 86f41839fe820bb775438c27a354f56361c2404e Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:19:24 +0530 Subject: [PATCH 309/550] fix: Added a message to enable appointment booking if disabled (#26233) * fix: Added a message to enable appointment booking if disabled * refactor: added translation for the message Co-authored-by: Rucha Mahabal * fix: added missing import * fix: minor identation and space fix Co-authored-by: Rucha Mahabal --- erpnext/www/book_appointment/index.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 7bfac89f30..4f455614ba 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -2,7 +2,7 @@ import frappe import datetime import json import pytz - +from frappe import _ WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -14,7 +14,8 @@ def get_context(context): if is_enabled: return context else: - frappe.local.flags.redirect_location = '/404' + frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"), + http_status_code=302, indicator_color="red") raise frappe.Redirect @frappe.whitelist(allow_guest=True) @@ -146,4 +147,4 @@ def _deltatime_to_datetime(date, deltatime): def _datetime_to_deltatime(date_time): midnight = datetime.datetime.combine(date_time.date(), datetime.time.min) - return (date_time-midnight) \ No newline at end of file + return (date_time-midnight) From 6c2f66b0a410337374d7c8ab4a3aad01ab34c63a Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:21:44 +0530 Subject: [PATCH 310/550] fix: Added permission for employee to book appointment (#26266) --- erpnext/crm/doctype/appointment/appointment.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 8517ddec32..016b8ec3e4 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -102,7 +102,7 @@ } ], "links": [], - "modified": "2020-01-28 16:16:45.447213", + "modified": "2021-06-30 12:09:14.228756", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -153,6 +153,18 @@ "role": "Sales User", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 } ], "quick_entry": 1, From ad6f20c5c778fc2377f4febf8389d575d6c87185 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:32:22 +0530 Subject: [PATCH 311/550] fix: Added permission for employee to book appointment (#26255) --- erpnext/crm/doctype/appointment/appointment.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 8517ddec32..306be7faa7 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -102,7 +102,7 @@ } ], "links": [], - "modified": "2020-01-28 16:16:45.447213", + "modified": "2021-06-29 18:27:02.832979", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -153,6 +153,18 @@ "role": "Sales User", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 } ], "quick_entry": 1, From 18533e381a832bfb78f08d34eb51731edaeacb05 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 2 Jul 2021 12:57:06 +0530 Subject: [PATCH 312/550] fix: lms progress issue (#26253) --- erpnext/education/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py index 9db8a4a90d..3070e6a3e8 100644 --- a/erpnext/education/utils.py +++ b/erpnext/education/utils.py @@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program): student = get_current_student() course_enrollment = get_enrollment("course", course, student.name) if not course_enrollment: - program_enrollment = get_enrollment('program', program, student.name) + program_enrollment = get_enrollment('program', program.name, student.name) if not program_enrollment: frappe.throw(_("You are not enrolled in program {0}").format(program)) return - return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name)) + return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name)) else: return frappe.get_doc('Course Enrollment', course_enrollment) From 0a15a03522eb88bcec5a0e478dbfb9d180f8ee7f Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 2 Jul 2021 13:06:56 +0530 Subject: [PATCH 313/550] fix: lms progress issue (#26250) Co-authored-by: Rucha Mahabal --- erpnext/education/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py index 9db8a4a90d..3070e6a3e8 100644 --- a/erpnext/education/utils.py +++ b/erpnext/education/utils.py @@ -355,11 +355,11 @@ def get_or_create_course_enrollment(course, program): student = get_current_student() course_enrollment = get_enrollment("course", course, student.name) if not course_enrollment: - program_enrollment = get_enrollment('program', program, student.name) + program_enrollment = get_enrollment('program', program.name, student.name) if not program_enrollment: frappe.throw(_("You are not enrolled in program {0}").format(program)) return - return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name)) + return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name)) else: return frappe.get_doc('Course Enrollment', course_enrollment) From 877597bc16c061a68e4577e434df6b1c041033c5 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Fri, 2 Jul 2021 13:10:18 +0530 Subject: [PATCH 314/550] fix: feating employee in payroll entry (#26271) --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 5c7c0a3b09..36e728fc99 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -680,6 +680,10 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] include_employees = [] emp_cond = '' + + if not filters.payroll_frequency: + frappe.throw(_('Select Payroll Frequency.')) + if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) emp = filters.get('employees') From 2e86d1301283e4ae8ed00abd66ace51f5aca1acf Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Fri, 2 Jul 2021 13:10:51 +0530 Subject: [PATCH 315/550] fix: feating employee in payroll entry (#26270) Co-authored-by: Rucha Mahabal From 4e6805b04ef48f47954f703e10dc95f525184541 Mon Sep 17 00:00:00 2001 From: Ashish Shah Date: Fri, 2 Jul 2021 13:35:04 +0530 Subject: [PATCH 316/550] fix: When Lead is created with mobile_no, mobile_no value gets lost (it is overwritten by phon value) (#26116) Steps to reproduce [1]Create a Lead. [2]Enter Person Name(lead_name): XX under Contact section, enter Phone(phone): 11 and Mobile No.(mobile_no):22 [3]Save it [4] F12, cur_frm.doc.phone : 11 (correct) cur_frm.doc.mobile_no : 11 (incorrect) [5]Under Address & Contact section ,check contact_html it shows ty Phone: 22 (correct) Phone: 11 (in correct) Actual: mobile_no value is lost. it is overwritten by phone value Expected: mobile_no value should be retained --- erpnext/crm/doctype/lead/lead.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index d1d096843b..ce3de40fc3 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -168,12 +168,13 @@ class Lead(SellingController): if self.phone: contact.append("phone_nos", { "phone": self.phone, - "is_primary": 1 + "is_primary_phone": 1 }) if self.mobile_no: contact.append("phone_nos", { - "phone": self.mobile_no + "phone": self.mobile_no, + "is_primary_mobile_no":1 }) contact.insert(ignore_permissions=True) From c0817838d951a56d8ea38f268a6e20d43ef05c71 Mon Sep 17 00:00:00 2001 From: Ashish Shah Date: Fri, 2 Jul 2021 15:16:42 +0530 Subject: [PATCH 317/550] fix: when lead is created with mobile_no, mobile_no value gets lost (#26298) Summary: When a Lead is created with mobile_no, mobile_no value gets lost (mobile_no value is overwritten by phone value) It is backport of https://github.com/frappe/erpnext/pull/26116 Steps to reproduce [1]Create a Lead. [2]Enter Person Name(lead_name): before_fix Under Contact section, enter Phone(phone): 11 and Mobile No.(mobile_no):22 [3]Save it [4] F12, cur_frm.doc.phone : 11 (correct) cur_frm.doc.mobile_no : 11 (incorrect, it should be 22) [5]Under Address & Contact section ,check contact_html it shows before_fix Phone: 11 (Primary label is missing) Phone: 22 (incorrect, it should be Mobile No:22, also Primary label is missing) Actual: mobile_no value is lost. it is overwritten by phone value following is image with error (before fix) ![image](https://user-images.githubusercontent.com/29812965/122664017-54b2e880-d1bc-11eb-8e4c-767a23ed7eb7.png) Expected: mobile_no value should be retained following is image after fix ![image](https://user-images.githubusercontent.com/29812965/122664037-64323180-d1bc-11eb-8f6f-7628cdaa7adc.png) --- erpnext/crm/doctype/lead/lead.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index d1d096843b..ce3de40fc3 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -168,12 +168,13 @@ class Lead(SellingController): if self.phone: contact.append("phone_nos", { "phone": self.phone, - "is_primary": 1 + "is_primary_phone": 1 }) if self.mobile_no: contact.append("phone_nos", { - "phone": self.mobile_no + "phone": self.mobile_no, + "is_primary_mobile_no":1 }) contact.insert(ignore_permissions=True) From 4cf3d9ac2064d367877b8f29a0177dab8c61ff86 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 2 Jul 2021 15:25:04 +0530 Subject: [PATCH 318/550] fix(Sales Invoice): Let invoice be created for Sold Assets if it's a return invoice --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 494f63f4a2..bfcf206757 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -149,7 +149,7 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled") or asset.status == "Sold" and not self.is_return: + elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return): frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) def validate_item_cost_centers(self): From 20f73d4c582766c0698370015340848f3d68fa32 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 2 Jul 2021 15:34:26 +0530 Subject: [PATCH 319/550] fix: only "Tax" type accounts should be shown for selection in GST Settings --- erpnext/regional/doctype/gst_settings/gst_settings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.js b/erpnext/regional/doctype/gst_settings/gst_settings.js index 808f9bc078..cd682c5403 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.js +++ b/erpnext/regional/doctype/gst_settings/gst_settings.js @@ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', { return { filters: { company: row.company, + account_type: "Tax", is_group: 0 } }; From b6076f772d6fd9928ddadb8572e391a7beb33c92 Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 2 Jul 2021 15:39:16 +0530 Subject: [PATCH 320/550] fix: only "Tax" type accounts should be shown for selection in GST Settings --- erpnext/regional/doctype/gst_settings/gst_settings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.js b/erpnext/regional/doctype/gst_settings/gst_settings.js index 808f9bc078..cd682c5403 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.js +++ b/erpnext/regional/doctype/gst_settings/gst_settings.js @@ -35,6 +35,7 @@ frappe.ui.form.on('GST Settings', { return { filters: { company: row.company, + account_type: "Tax", is_group: 0 } }; From f8ee8058e7cba69882e953d45f512763f81c3518 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 2 Jul 2021 15:45:52 +0530 Subject: [PATCH 321/550] fix(Sales Invoice): Reset disposal_date on returning the Asset --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bfcf206757..dc5282037a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -932,15 +932,16 @@ class SalesInvoice(SellingController): if self.is_return: fixed_asset_gl_entries = get_gl_entries_on_asset_movement(asset, item.base_net_amount, item.finance_book, True) + asset.db_set("disposal_date", None) else: fixed_asset_gl_entries = get_gl_entries_on_asset_movement(asset, item.base_net_amount, item.finance_book) + asset.db_set("disposal_date", self.posting_date) for gle in fixed_asset_gl_entries: gle["against"] = self.customer gl_entries.append(self.get_gl_dict(gle, item=item)) - asset.db_set("disposal_date", self.posting_date) self.set_asset_status(asset) else: From 4503a3836128784541ef14a575f9067db182438c Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Jul 2021 17:13:45 +0530 Subject: [PATCH 322/550] fix: Handle Stock Reco cancellation and limit reposting - Handled cancellation of reco with and without prior SLE - Repost / Recalculate balance qty only till next stock reco --- .../stock_reconciliation.py | 1 + erpnext/stock/stock_ledger.py | 84 ++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 2b51c1a5c3..7b84c4c2d9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -357,6 +357,7 @@ class StockReconciliation(StockController): if row.current_qty: data.actual_qty = -1 * row.current_qty data.qty_after_transaction = flt(row.current_qty) + data.previous_qty_after_transaction = flt(row.qty) data.valuation_rate = flt(row.current_valuation_rate) data.stock_value = data.qty_after_transaction * data.valuation_rate data.stock_value_difference = -1 * flt(row.amount_difference) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 94bd3077a7..7425473f9d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -55,6 +55,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() + + if sle.get("voucher_type") == "Stock Reconciliation": + # preserve previous_qty_after_transaction for qty reposting + args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") + update_bin(args, allow_negative_stock, via_landed_cost_voucher) def get_args_for_future_sle(row): @@ -869,19 +874,21 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, def update_qty_in_future_sle(args, allow_negative_stock=None): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" + datetime_limit_condition = "" + last_balance = None + qty_shift = args.actual_qty # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": - last_balance = get_previous_sle_of_current_voucher( - args, - exclude_current_voucher=True - ).get("qty_after_transaction") - if last_balance is not None: - stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) - else: - stock_reco_qty_shift = args.qty_after_transaction - qty_shift = stock_reco_qty_shift + qty_shift = get_stock_reco_qty_shift(args) + + # find the next nearest stock reco so that we only recalculate SLEs till that point + next_stock_reco_detail = get_next_stock_reco(args) + if next_stock_reco_detail: + detail = next_stock_reco_detail[0] + # add condition to update SLEs before this date & time + datetime_limit_condition = get_datetime_limit_condition(detail) frappe.db.sql(""" update `tabStock Ledger Entry` @@ -897,10 +904,67 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): and creation > %(creation)s ) ) - """.format(qty_shift=qty_shift), args) + {datetime_limit_condition} + """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args) validate_negative_qty_in_future_sle(args, allow_negative_stock) +def get_stock_reco_qty_shift(args): + stock_reco_qty_shift = 0 + if args.get("is_cancelled"): + if args.get("previous_qty_after_transaction"): + # get qty (balance) that was set at submission + last_balance = args.get("previous_qty_after_transaction") + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = flt(args.actual_qty) + else: + # reco is being submitted + last_balance = get_previous_sle_of_current_voucher(args, + exclude_current_voucher=True).get("qty_after_transaction") + + if last_balance is not None: + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = args.qty_after_transaction + + return stock_reco_qty_shift + +def get_next_stock_reco(args): + """Returns next nearest stock reconciliaton's details.""" + + return frappe.db.sql(""" + select + name, posting_date, posting_time, creation, voucher_no + from + `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_type = 'Stock Reconciliation' + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) + or ( + timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) + and creation > %(creation)s + ) + ) + limit 1 + """, args, as_dict=1) + +def get_datetime_limit_condition(detail): + if not detail: return None + + return f""" + and + (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') + or ( + timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}') + and creation < '{detail.creation}' + ) + )""" + def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): allow_negative_stock = allow_negative_stock \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) From 0a79cfa17044ec0e8f8ccbb31107ed39fa52edc0 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 2 Jul 2021 17:54:43 +0530 Subject: [PATCH 323/550] fix: set query for training events (#26302) * fix: set query * fix: whitespace between function name and param Co-authored-by: Rucha Mahabal --- .../hr/doctype/training_event/training_event.js | 14 ++++++++++---- .../training_event_employee.json | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index b7d34b178a..064dfb2455 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', { frappe.set_route("List", "Training Feedback"); }); } - } -}); + frm.events.set_employee_query(frm); + }, -frappe.ui.form.on("Training Event Employee", { - employee: function (frm) { + set_employee_query: function(frm) { let emp = []; for (let d in frm.doc.employees) { if (frm.doc.employees[d].employee) { @@ -40,3 +39,10 @@ frappe.ui.form.on("Training Event Employee", { }); } }); + +frappe.ui.form.on("Training Event Employee", { + employee: function(frm) { + frm.events.set_employee_query(frm); + } +}); + diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json index 2d313e9fac..bcb7d5e5bc 100644 --- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json +++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json @@ -19,6 +19,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Employee", + "no_copy": 1, "options": "Employee" }, { @@ -68,7 +69,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-21 12:41:59.336237", + "modified": "2021-07-02 17:20:27.630176", "modified_by": "Administrator", "module": "HR", "name": "Training Event Employee", From 73db919c99d376abbea9d98b2f6e53a8e46ff4ed Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 2 Jul 2021 17:55:42 +0530 Subject: [PATCH 324/550] fix: set query for training events (#26303) * fix: set query * fix: remove whitespace between function and params Co-authored-by: Rucha Mahabal --- .../hr/doctype/training_event/training_event.js | 14 ++++++++++---- .../training_event_employee.json | 3 ++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index b7d34b178a..064dfb2455 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -20,11 +20,10 @@ frappe.ui.form.on('Training Event', { frappe.set_route("List", "Training Feedback"); }); } - } -}); + frm.events.set_employee_query(frm); + }, -frappe.ui.form.on("Training Event Employee", { - employee: function (frm) { + set_employee_query: function(frm) { let emp = []; for (let d in frm.doc.employees) { if (frm.doc.employees[d].employee) { @@ -40,3 +39,10 @@ frappe.ui.form.on("Training Event Employee", { }); } }); + +frappe.ui.form.on("Training Event Employee", { + employee: function(frm) { + frm.events.set_employee_query(frm); + } +}); + diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json index 2d313e9fac..bcb7d5e5bc 100644 --- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json +++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json @@ -19,6 +19,7 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Employee", + "no_copy": 1, "options": "Employee" }, { @@ -68,7 +69,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-21 12:41:59.336237", + "modified": "2021-07-02 17:20:27.630176", "modified_by": "Administrator", "module": "HR", "name": "Training Event Employee", From 311e277204a6aa28a6a132a888787385018e546f Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Jul 2021 17:46:05 +0530 Subject: [PATCH 325/550] fix: Sider --- erpnext/stock/stock_ledger.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7425473f9d..4e9c7689ae 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -875,8 +875,6 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, def update_qty_in_future_sle(args, allow_negative_stock=None): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" - last_balance = None - qty_shift = args.actual_qty # find difference/shift in qty caused by stock reconciliation @@ -937,7 +935,7 @@ def get_next_stock_reco(args): select name, posting_date, posting_time, creation, voucher_no from - `tabStock Ledger Entry` + `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s @@ -954,8 +952,6 @@ def get_next_stock_reco(args): """, args, as_dict=1) def get_datetime_limit_condition(detail): - if not detail: return None - return f""" and (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') From 8db974b1b9304a53d402b14672f1306d9c64ca3c Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 2 Jul 2021 19:35:50 +0530 Subject: [PATCH 326/550] test: updated test cases --- .../buying/doctype/supplier/test_supplier.py | 24 ++++++++------- .../selling/doctype/customer/test_customer.py | 30 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index faa813aa4c..8980466270 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -14,7 +14,8 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): def test_get_supplier_group_details(self): - doc = frappe.get_doc("Supplier Group", "Local") + doc = frappe.new_doc("Supplier Group") + doc.supplier_group_name = "_Testing Supplier Group" doc.payment_terms = "_Test Payment Term Template 3" doc.accounts = [] test_account_details = { @@ -23,15 +24,18 @@ class TestSupplier(unittest.TestCase): } doc.append("accounts", test_account_details) doc.save() - doc = frappe.get_doc("Supplier", "_Test Supplier") - doc.supplier_group = "Local" - doc.payment_terms = "" - doc.accounts = [] - doc.save() - doc.get_supplier_group_details() - self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(doc.accounts[0].company, "_Test Company") - self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + s_doc = frappe.new_doc("Supplier") + s_doc.supplier_name = "Testing Supplier" + s_doc.supplier_group = "_Testing Supplier Group" + s_doc.payment_terms = "" + s_doc.accounts = [] + s_doc.insert() + s_doc.get_supplier_group_details() + self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(s_doc.accounts[0].company, "_Test Company") + self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") + s_doc.delete() + doc.delete() def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 8cb07aaa8a..b1a5b52f96 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -28,7 +28,8 @@ class TestCustomer(unittest.TestCase): set_credit_limit('_Test Customer', '_Test Company', 0) def test_get_customer_group_details(self): - doc = frappe.get_doc("Customer Group", "Commercial") + doc = frappe.new_doc("Customer Group") + doc.customer_group_name = "_Testing Customer Group" doc.payment_terms = "_Test Payment Term Template 3" doc.accounts = [] doc.default_price_list = "Standard Buying" @@ -43,21 +44,24 @@ class TestCustomer(unittest.TestCase): } doc.append("accounts", test_account_details) doc.append("credit_limits", test_credit_limits) - doc.save() + doc.insert() - doc = frappe.get_doc("Customer", "_Test Customer") - doc.customer_group = "Commercial" - doc.payment_terms = doc.default_price_list = "" - doc.accounts = doc.credit_limits= [] - doc.save() - doc.get_customer_group_details() - self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + c_doc = frappe.new_doc("Customer") + c_doc.customer_name = "Testing Customer" + c_doc.customer_group = "_Testing Customer Group" + c_doc.payment_terms = c_doc.default_price_list = "" + c_doc.accounts = c_doc.credit_limits= [] + c_doc.insert() + c_doc.get_customer_group_details() + self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(doc.accounts[0].company, "_Test Company") - self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + self.assertEqual(c_doc.accounts[0].company, "_Test Company") + self.assertEqual(c_doc.accounts[0].account, "Creditors - _TC") - self.assertEqual(doc.credit_limits[0].company, "_Test Company") - self.assertEqual(doc.credit_limits[0].credit_limit, 350000 ) + self.assertEqual(c_doc.credit_limits[0].company, "_Test Company") + self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000) + c_doc.delete() + doc.delete() def test_party_details(self): from erpnext.accounts.party import get_party_details From 3105332e3c707af1360e529ec77e438897cc9e2f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 3 Jul 2021 17:22:09 +0530 Subject: [PATCH 327/550] fix: allow to make job card without employee --- .../doctype/job_card/job_card.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 7f8f2ef68d..420bb00803 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -192,15 +192,20 @@ class JobCard(Document): "completed_qty": args.get("completed_qty") or 0.0 }) elif args.get("start_time"): - for name in employees: - self.append("time_logs", { - "from_time": get_datetime(args.get("start_time")), - "employee": name.get('employee'), - "operation": args.get("sub_operation"), - "completed_qty": 0.0 - }) + new_args = { + "from_time": get_datetime(args.get("start_time")), + "operation": args.get("sub_operation"), + "completed_qty": 0.0 + } - if not self.employee: + if employees: + for name in employees: + new_args.employee = name.get('employee') + self.add_start_time_log(new_args) + else: + self.add_start_time_log(new_args) + + if not self.employee and employees: self.set_employees(employees) if self.status == "On Hold": @@ -208,6 +213,9 @@ class JobCard(Document): self.save() + def add_start_time_log(self, args): + self.append("time_logs", args) + def set_employees(self, employees): for name in employees: self.append('employee', { From 802e63a9d77af37abb29c8a28e093d0a08a89612 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Jun 2021 11:24:32 +0530 Subject: [PATCH 328/550] fix: Do not consider cancelled entries in party dashboard --- erpnext/accounts/party.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e025fc6905..b97dc401e6 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -542,6 +542,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where party_type = %s and party=%s + and is_cancelled = 0 group by company""", (party_type, party))) for d in companies: From 5069095984270bd7599fce078444557489523e84 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 09:31:31 +0530 Subject: [PATCH 329/550] fix: Auto process deferred accounting for multi-company setup --- erpnext/accounts/deferred_revenue.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 2f86c6c1de..335e8a15ab 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -301,17 +301,21 @@ def process_deferred_accounting(posting_date=None): start_date = add_months(today(), -1) end_date = add_days(today(), -1) - for record_type in ('Income', 'Expense'): - doc = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=posting_date, - start_date=start_date, - end_date=end_date, - type=record_type - )) + companies = frappe.get_all('Company') - doc.insert() - doc.submit() + for company in companies: + for record_type in ('Income', 'Expense'): + doc = frappe.get_doc(dict( + doctype='Process Deferred Accounting', + company=company.name, + posting_date=posting_date, + start_date=start_date, + end_date=end_date, + type=record_type + )) + + doc.insert() + doc.submit() def make_gl_entries(doc, credit_account, debit_account, against, amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None): From 9d295ca93972679773f1de806a2992819250a1c0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 18:56:51 +0530 Subject: [PATCH 330/550] fix: Bank statement import --- .../doctype/bank_statement_import/bank_statement_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 5f110e2727..ffc9d1c465 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -51,7 +51,7 @@ class BankStatementImport(DataImport): self.import_file, self.google_sheets_url ) - if 'Bank Account' not in json.dumps(preview): + if 'Bank Account' not in json.dumps(preview['columns']): frappe.throw(_("Please add the Bank Account column")) from frappe.core.page.background_jobs.background_jobs import get_info From a0599e5ac2d4da456256ab00bbe2105e03d3e821 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 10:09:42 +0530 Subject: [PATCH 331/550] fix: Test cases for M-pesa --- .../doctype/mpesa_settings/test_mpesa_settings.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index 3c2e59ab82..2dfd5aad5d 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -9,13 +9,17 @@ from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import p from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice class TestMpesaSettings(unittest.TestCase): + def setUp(self): + # create payment gateway in setup + create_mpesa_settings(payment_gateway_name="_Test") + create_mpesa_settings(payment_gateway_name="_Account Balance") + create_mpesa_settings(payment_gateway_name="Payment") + def tearDown(self): frappe.db.sql('delete from `tabMpesa Settings`') frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"') def test_creation_of_payment_gateway(self): - create_mpesa_settings(payment_gateway_name="_Test") - mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test") self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"})) self.assertTrue(mode_of_payment.name) @@ -47,7 +51,6 @@ class TestMpesaSettings(unittest.TestCase): integration_request.delete() def test_processing_of_callback_payload(self): - create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") @@ -90,7 +93,6 @@ class TestMpesaSettings(unittest.TestCase): pos_invoice.delete() def test_processing_of_multiple_callback_payload(self): - create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") @@ -141,7 +143,6 @@ class TestMpesaSettings(unittest.TestCase): pos_invoice.delete() def test_processing_of_only_one_succes_callback_payload(self): - create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") From 75fdf79376ccec55e34cf0ff1911f297360a431d Mon Sep 17 00:00:00 2001 From: Richard Case <64409021+casesolved-co-uk@users.noreply.github.com> Date: Mon, 5 Jul 2021 06:26:34 +0100 Subject: [PATCH 332/550] fix: incorrect bom no. added for non-variant items on variant boms (#26320) --- erpnext/manufacturing/doctype/bom/bom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c31b1bd3e9..c32a8a95a1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1115,6 +1115,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): }, 'BOM Item': { 'doctype': 'BOM Item', + # stop get_mapped_doc copying parent bom_no to children + 'field_no_map': ['bom_no'], 'condition': lambda doc: doc.has_variants == 0 }, }, target_doc, postprocess) From 046e83bf5040d690b546d0499099baf80fe6ad68 Mon Sep 17 00:00:00 2001 From: Richard Case <64409021+casesolved-co-uk@users.noreply.github.com> Date: Mon, 5 Jul 2021 06:26:34 +0100 Subject: [PATCH 333/550] fix: incorrect bom no. added for non-variant items on variant boms (#26320) --- erpnext/manufacturing/doctype/bom/bom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c58f017258..3bd1fe6c7f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1100,6 +1100,8 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): }, 'BOM Item': { 'doctype': 'BOM Item', + # stop get_mapped_doc copying parent bom_no to children + 'field_no_map': ['bom_no'], 'condition': lambda doc: doc.has_variants == 0 }, }, target_doc, postprocess) From 03f7bf6589b9d0df64368339a79ad90807497bb2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 5 Jul 2021 11:48:36 +0530 Subject: [PATCH 334/550] test: variant BOM from template BOM --- erpnext/manufacturing/doctype/bom/test_bom.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 57a5458726..c89f7d66fd 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -8,6 +8,7 @@ import frappe from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation +from erpnext.manufacturing.doctype.bom.bom import make_variant_bom from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -248,6 +249,37 @@ class TestBOM(unittest.TestCase): for reqd_item, created_item in zip(reqd_order, created_order): self.assertEqual(reqd_item, created_item.item_code) + def test_generated_variant_bom(self): + from erpnext.controllers.item_variant import create_variant + + template_item = make_item( + "_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]} + ) + variant = create_variant(template_item.item_code, {"Test Size": "Large"}) + variant.insert(ignore_if_duplicate=True) + + bom_tree = { + template_item.item_code: { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "ChildPart5": {}, + } + } + template_bom = create_nested_bom(bom_tree, prefix="") + variant_bom = make_variant_bom( + template_bom.name, template_bom.name, variant.item_code, variant_items=[] + ) + variant_bom.save() + + reqd_order = template_bom.get_tree_representation().level_order_traversal() + created_order = variant_bom.get_tree_representation().level_order_traversal() + + self.assertEqual(len(reqd_order), len(created_order)) + + for reqd_item, created_item in zip(reqd_order, created_order): + self.assertEqual(reqd_item.item_code, created_item.item_code) + self.assertEqual(reqd_item.qty, created_item.qty) + self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From db682d9e4cdea680f2ab0d4e589000e54baf6a20 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 13:46:03 +0530 Subject: [PATCH 335/550] fix: Create mode of payment if doesn't exists --- .../doctype/mpesa_settings/test_mpesa_settings.py | 3 ++- erpnext/erpnext_integrations/utils.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index 2dfd5aad5d..f592c180a3 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -7,6 +7,7 @@ import frappe import unittest from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice +from erpnext.erpnext_integrations.utils import create_mode_of_payment class TestMpesaSettings(unittest.TestCase): def setUp(self): @@ -20,7 +21,7 @@ class TestMpesaSettings(unittest.TestCase): frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"') def test_creation_of_payment_gateway(self): - mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test") + mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone") self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"})) self.assertTrue(mode_of_payment.name) self.assertEqual(mode_of_payment.type, "Phone") diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index 3840e781b4..b764701103 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"): "payment_gateway": gateway }, ['payment_account']) - if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account: + mode_of_payment = frappe.db.exists("Mode of Payment", gateway) + if not mode_of_payment and payment_gateway_account: mode_of_payment = frappe.get_doc({ "doctype": "Mode of Payment", "mode_of_payment": gateway, @@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"): }) mode_of_payment.insert(ignore_permissions=True) + return mode_of_payment + else: + return frappe.get_doc("Mode of Payment", mode_of_payment) + def get_tracking_url(carrier, tracking_number): # Return the formatted Tracking URL. tracking_url = '' From 5638fbb1aac81bc1196c9a942e1abb9310983ce6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 5 Jul 2021 13:54:05 +0530 Subject: [PATCH 336/550] fix: bom stock report not working --- .../manufacturing/report/bom_stock_report/bom_stock_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 1c6758e6f3..ed8b93929a 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -70,12 +70,12 @@ def get_bom_stock(filters): ON bom_item.item_code = ledger.item_code {conditions} WHERE - bom_item.parent = '{bom}' and bom_item.parenttype='BOM' + bom_item.parent = {bom} and bom_item.parenttype='BOM' GROUP BY bom_item.item_code""".format( qty_field=qty_field, table=table, conditions=conditions, - bom=bom, + bom=frappe.db.escape(bom), qty_to_produce=qty_to_produce or 1) ) From c69bc54297314f952458b4d1bd30b8524b61cad2 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 14:24:38 +0530 Subject: [PATCH 337/550] fix: Validate LCV for Invoices without Update Stock --- .../landed_cost_voucher/landed_cost_voucher.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 5df4d8743f..1f78867bef 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -60,8 +60,19 @@ class LandedCostVoucher(Document): receipt_documents = [] for d in self.get("purchase_receipts"): - if frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") != 1: - frappe.throw(_("Receipt document must be submitted")) + doc_data = frappe.db.get_values( + d.receipt_document_type, + d.receipt_document, + ["docstatus", "update_stock"], + as_dict=1 + )[0] + if doc_data.get("docstatus") != 1: + msg = f"Row {d.idx}: Receipt Document {frappe.bold(d.receipt_document)} must be submitted" + frappe.throw(_(msg), title=_("Invalid Document")) + elif d.receipt_document_type == "Purchase Invoice" and not doc_data.get("update_stock"): + msg = _(f"Row {d.idx}: Purchase Invoice {frappe.bold(d.receipt_document)} has no stock impact.") + msg += "
    " + _("Please create Landed Cost Vouchers against Invoices with 'Update Stock' enabled.") + frappe.throw(msg, title=_("Incorrect Invoice")) else: receipt_documents.append(d.receipt_document) From 15b336df28c6a8746bea4edb362400bd6c1f8c3d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 14:45:33 +0530 Subject: [PATCH 338/550] fix: Test cases --- erpnext/erpnext_integrations/utils.py | 2 +- erpnext/hr/doctype/expense_claim/test_expense_claim.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index b764701103..a5e162f8b5 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -68,7 +68,7 @@ def create_mode_of_payment(gateway, payment_type="General"): mode_of_payment.insert(ignore_permissions=True) return mode_of_payment - else: + elif mode_of_payment: return frappe.get_doc("Mode of Payment", mode_of_payment) def get_tracking_url(carrier, tracking_number): diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 578eccf787..141561fcdc 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_gl_entry(self): payable_account = get_payable_account(company_name) taxes = generate_taxes() - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes) + expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", + do_not_submit=True, taxes=taxes) expense_claim.submit() gl_entries = frappe.db.sql("""select account, debit, credit @@ -145,7 +146,7 @@ def generate_taxes(): parent_account = frappe.db.get_value('Account', {'company': company_name, 'is_group':1, 'account_type': 'Tax'}, 'name') - account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account) + account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account) return {'taxes':[{ "account_head": account, "rate": 0, From 9b6d9a41f4a66fefcb02e7ec0c4f3a4100c4d3ac Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 17:08:27 +0530 Subject: [PATCH 339/550] fix: Test Cases --- .../doctype/mpesa_settings/test_mpesa_settings.py | 1 + erpnext/hr/doctype/expense_claim/test_expense_claim.py | 2 +- erpnext/setup/setup_wizard/operations/install_fixtures.py | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index f592c180a3..b0e662d3f3 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -204,6 +204,7 @@ def create_mpesa_settings(payment_gateway_name="Express"): doc = frappe.get_doc(dict( #nosec doctype="Mpesa Settings", + sandbox=1, payment_gateway_name=payment_gateway_name, consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn", consumer_secret="VI1oS3oBGPJfh3JyvLHw", diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 141561fcdc..96ea686706 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -83,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase): self.assertTrue(gl_entries) expected_values = dict((d[0], d) for d in [ - ['CGST - _TC4',18.0, 0.0], + ['Output Tax CGST - _TC4',18.0, 0.0], [payable_account, 0.0, 218.0], ["Travel Expenses - _TC4", 200.0, 0.0] ]) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 7dfb9f4d3c..3dcb63867c 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -449,6 +449,8 @@ def install_defaults(args=None): set_active_domains(args) update_stock_settings() update_shopping_cart_settings(args) + + args.update({"set_default": 1}) create_bank_account(args) def set_global_defaults(args): @@ -499,7 +501,10 @@ def create_bank_account(args): try: doc = bank_account.insert() - frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) + if args.get('set_default'): + frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) + + return doc except RootNotEditable: frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account'))) From fa9e67502cf538e56b42e89e03878a9766b034f6 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 20:23:00 +0530 Subject: [PATCH 340/550] chore: Test for backdated reco qty reposting --- .../test_stock_reconciliation.py | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 36380b838b..f7b243221a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, unittest -from frappe.utils import flt, nowdate, nowtime +from frappe.utils import flt, nowdate, nowtime, add_days from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items @@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt class TestStockReconciliation(unittest.TestCase): @classmethod @@ -204,6 +205,74 @@ class TestStockReconciliation(unittest.TestCase): self.assertEqual(sr.get("items")[0].valuation_rate, 0) self.assertEqual(sr.get("items")[0].amount, 0) + def test_backdated_stock_reco_qty_reposting(self): + """ + Test if a backdated stock reco recalculates future qty until next reco. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] + PR1 | PR | 10 | 18 (posting date: today-3) + PR2 | PR | 1 | 19 (posting date: today-2) + SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] + PR3 | PR | 1 | 7 (posting date: today) # can't post future PR + """ + item_code = "Backdated-Reco-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, + posting_date=add_days(nowdate(), -3)) + pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=add_days(nowdate(), -2)) + pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=nowdate()) + + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(pr3_balance, 12) + + # post backdated stock reco in between + sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100, + posting_date=add_days(nowdate(), -1)) + pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr3_balance, 7) + + # post backdated stock reco at the start + sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100, + posting_date=add_days(nowdate(), -4)) + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, + "qty_after_transaction") + sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 18) + self.assertEqual(pr2_balance, 19) + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + + # cancel backdated stock reco and check future impact + sr5.cancel() + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, + "qty_after_transaction") + sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(pr2_balance, 11) + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + + # teardown + sr4.cancel() + pr3.cancel() + pr2.cancel() + pr1.cancel() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From 8c1b764a90ed543df6d350e1184128354f5f6311 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 21:49:10 +0530 Subject: [PATCH 341/550] fix: Debug tests --- .../doctype/purchase_invoice/test_purchase_invoice.py | 1 + .../test_service_level_agreement.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ec93314c0f..2ce65c13bf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1042,6 +1042,7 @@ class TestPurchaseInvoice(unittest.TestCase): where voucher_type='Purchase Invoice' and voucher_no=%s order by account asc""", (purchase_invoice.name), as_dict=1) + print(gl_entries) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.debit) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 2a8446d29f..4ef4930659 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -81,6 +81,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA custom fields created for leads sla_fields = get_service_level_agreement_fields() + frappe.clear_cache(doctype=doctype) meta = frappe.get_meta(doctype, cached=False) for field in sla_fields: @@ -219,9 +220,9 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') - def tearDown(self): - for d in frappe.get_all("Service Level Agreement"): - frappe.delete_doc("Service Level Agreement", d.name, force=1) + # def tearDown(self): + # for d in frappe.get_all("Service Level Agreement"): + # frappe.delete_doc("Service Level Agreement", d.name, force=1) def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue"): From 57d06a86f8b2ab481cf5c2cb6d8a3f7c40c05ec5 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 21:56:03 +0530 Subject: [PATCH 342/550] chore: Test to block backdated reco causing future scarcity --- .../test_stock_reconciliation.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index cd891c0ed6..84cdc49128 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -309,6 +309,49 @@ class TestStockReconciliation(unittest.TestCase): pr2.cancel() pr1.cancel() + def test_backdated_stock_reco_future_negative_stock(self): + """ + Test if a backdated stock reco causes future negative stock and is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + PR1 | PR | 10 | 10 (posting date: today-2) + SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] + DN2 | DN | -2 | 8(-1) (posting date: today) + """ + from erpnext.stock.stock_ledger import NegativeStockError + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = "Backdated-Reco-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0) + + pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, + posting_date=add_days(nowdate(), -2)) + dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120, + posting_date=nowdate()) + + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(dn2_balance, 8) + + # check if stock reco is blocked + sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=add_days(nowdate(), -1), do_not_submit=True) + self.assertRaises(NegativeStockError, sr3.submit) + + # teardown + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting) + sr3.cancel() + dn2.cancel() + pr1.cancel() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From 8418c4bfe00b926d0943194a16529150aae19cc6 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 22 Jun 2021 21:35:25 +0530 Subject: [PATCH 343/550] fix: Include Stock Reco logic in update_qty_in_future_sle --- erpnext/stock/stock_ledger.py | 75 ++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9fe89c3fa5..94bd3077a7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -215,7 +215,7 @@ class update_entries_after(object): """ self.data.setdefault(args.warehouse, frappe._dict()) warehouse_dict = self.data[args.warehouse] - previous_sle = self.get_previous_sle_of_current_voucher(args) + previous_sle = get_previous_sle_of_current_voucher(args) warehouse_dict.previous_sle = previous_sle for key in ("qty_after_transaction", "valuation_rate", "stock_value"): @@ -227,29 +227,6 @@ class update_entries_after(object): "stock_value_difference": 0.0 }) - def get_previous_sle_of_current_voucher(self, args): - """get stock ledger entries filtered by specific posting datetime conditions""" - - args['time_format'] = '%H:%i:%s' - if not args.get("posting_date"): - args["posting_date"] = "1900-01-01" - if not args.get("posting_time"): - args["posting_time"] = "00:00" - - sle = frappe.db.sql(""" - select *, timestamp(posting_date, posting_time) as "timestamp" - from `tabStock Ledger Entry` - where item_code = %(item_code)s - and warehouse = %(warehouse)s - and is_cancelled = 0 - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) - order by timestamp(posting_date, posting_time) desc, creation desc - limit 1 - for update""", args, as_dict=1) - - return sle[0] if sle else frappe._dict() - - def build(self): from erpnext.controllers.stock_controller import future_sle_exists @@ -734,6 +711,35 @@ class update_entries_after(object): bin_doc.flags.via_stock_ledger_entry = True bin_doc.save(ignore_permissions=True) + +def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): + """get stock ledger entries filtered by specific posting datetime conditions""" + + args['time_format'] = '%H:%i:%s' + if not args.get("posting_date"): + args["posting_date"] = "1900-01-01" + if not args.get("posting_time"): + args["posting_time"] = "00:00" + + voucher_condition = "" + if exclude_current_voucher: + voucher_no = args.get("voucher_no") + voucher_condition = f"and voucher_no != '{voucher_no}'" + + sle = frappe.db.sql(""" + select *, timestamp(posting_date, posting_time) as "timestamp" + from `tabStock Ledger Entry` + where item_code = %(item_code)s + and warehouse = %(warehouse)s + and is_cancelled = 0 + {voucher_condition} + and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + order by timestamp(posting_date, posting_time) desc, creation desc + limit 1 + for update""".format(voucher_condition=voucher_condition), args, as_dict=1) + + return sle[0] if sle else frappe._dict() + def get_previous_sle(args, for_update=False): """ get the last sle on or before the current time-bucket, @@ -862,9 +868,24 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate def update_qty_in_future_sle(args, allow_negative_stock=None): + """Recalculate Qty after Transaction in future SLEs based on current SLE.""" + qty_shift = args.actual_qty + + # find difference/shift in qty caused by stock reconciliation + if args.voucher_type == "Stock Reconciliation": + last_balance = get_previous_sle_of_current_voucher( + args, + exclude_current_voucher=True + ).get("qty_after_transaction") + if last_balance is not None: + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = args.qty_after_transaction + qty_shift = stock_reco_qty_shift + frappe.db.sql(""" update `tabStock Ledger Entry` - set qty_after_transaction = qty_after_transaction + {qty} + set qty_after_transaction = qty_after_transaction + {qty_shift} where item_code = %(item_code)s and warehouse = %(warehouse)s @@ -876,7 +897,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): and creation > %(creation)s ) ) - """.format(qty=args.actual_qty), args) + """.format(qty_shift=qty_shift), args) validate_negative_qty_in_future_sle(args, allow_negative_stock) @@ -884,7 +905,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): allow_negative_stock = allow_negative_stock \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) - if args.actual_qty < 0 and not allow_negative_stock: + if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: sle = get_future_sle_with_negative_qty(args) if sle: message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( From 4038977e2ed3f5e33b8c7151896c79ed3043981b Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Jul 2021 17:13:45 +0530 Subject: [PATCH 344/550] fix: Handle Stock Reco cancellation and limit reposting - Handled cancellation of reco with and without prior SLE - Repost / Recalculate balance qty only till next stock reco --- .../stock_reconciliation.py | 1 + erpnext/stock/stock_ledger.py | 84 ++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 2956384a67..3e15d547e0 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -357,6 +357,7 @@ class StockReconciliation(StockController): if row.current_qty: data.actual_qty = -1 * row.current_qty data.qty_after_transaction = flt(row.current_qty) + data.previous_qty_after_transaction = flt(row.qty) data.valuation_rate = flt(row.current_valuation_rate) data.stock_value = data.qty_after_transaction * data.valuation_rate data.stock_value_difference = -1 * flt(row.amount_difference) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 94bd3077a7..7425473f9d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -55,6 +55,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() + + if sle.get("voucher_type") == "Stock Reconciliation": + # preserve previous_qty_after_transaction for qty reposting + args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") + update_bin(args, allow_negative_stock, via_landed_cost_voucher) def get_args_for_future_sle(row): @@ -869,19 +874,21 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, def update_qty_in_future_sle(args, allow_negative_stock=None): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" + datetime_limit_condition = "" + last_balance = None + qty_shift = args.actual_qty # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": - last_balance = get_previous_sle_of_current_voucher( - args, - exclude_current_voucher=True - ).get("qty_after_transaction") - if last_balance is not None: - stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) - else: - stock_reco_qty_shift = args.qty_after_transaction - qty_shift = stock_reco_qty_shift + qty_shift = get_stock_reco_qty_shift(args) + + # find the next nearest stock reco so that we only recalculate SLEs till that point + next_stock_reco_detail = get_next_stock_reco(args) + if next_stock_reco_detail: + detail = next_stock_reco_detail[0] + # add condition to update SLEs before this date & time + datetime_limit_condition = get_datetime_limit_condition(detail) frappe.db.sql(""" update `tabStock Ledger Entry` @@ -897,10 +904,67 @@ def update_qty_in_future_sle(args, allow_negative_stock=None): and creation > %(creation)s ) ) - """.format(qty_shift=qty_shift), args) + {datetime_limit_condition} + """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args) validate_negative_qty_in_future_sle(args, allow_negative_stock) +def get_stock_reco_qty_shift(args): + stock_reco_qty_shift = 0 + if args.get("is_cancelled"): + if args.get("previous_qty_after_transaction"): + # get qty (balance) that was set at submission + last_balance = args.get("previous_qty_after_transaction") + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = flt(args.actual_qty) + else: + # reco is being submitted + last_balance = get_previous_sle_of_current_voucher(args, + exclude_current_voucher=True).get("qty_after_transaction") + + if last_balance is not None: + stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) + else: + stock_reco_qty_shift = args.qty_after_transaction + + return stock_reco_qty_shift + +def get_next_stock_reco(args): + """Returns next nearest stock reconciliaton's details.""" + + return frappe.db.sql(""" + select + name, posting_date, posting_time, creation, voucher_no + from + `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_type = 'Stock Reconciliation' + and voucher_no != %(voucher_no)s + and is_cancelled = 0 + and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) + or ( + timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) + and creation > %(creation)s + ) + ) + limit 1 + """, args, as_dict=1) + +def get_datetime_limit_condition(detail): + if not detail: return None + + return f""" + and + (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') + or ( + timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}') + and creation < '{detail.creation}' + ) + )""" + def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): allow_negative_stock = allow_negative_stock \ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) From 8c441263f81c1581bcecbf84c091e4b68c3cf153 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Jul 2021 17:46:05 +0530 Subject: [PATCH 345/550] fix: Sider --- erpnext/stock/stock_ledger.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 7425473f9d..4e9c7689ae 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -875,8 +875,6 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, def update_qty_in_future_sle(args, allow_negative_stock=None): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" - last_balance = None - qty_shift = args.actual_qty # find difference/shift in qty caused by stock reconciliation @@ -937,7 +935,7 @@ def get_next_stock_reco(args): select name, posting_date, posting_time, creation, voucher_no from - `tabStock Ledger Entry` + `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s @@ -954,8 +952,6 @@ def get_next_stock_reco(args): """, args, as_dict=1) def get_datetime_limit_condition(detail): - if not detail: return None - return f""" and (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') From e8c9ab4b01ff86eb72a9ff536f4f7d05b0dd8313 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 20:23:00 +0530 Subject: [PATCH 346/550] chore: Test for backdated reco qty reposting --- .../test_stock_reconciliation.py | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 36380b838b..f7b243221a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, unittest -from frappe.utils import flt, nowdate, nowtime +from frappe.utils import flt, nowdate, nowtime, add_days from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items @@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt class TestStockReconciliation(unittest.TestCase): @classmethod @@ -204,6 +205,74 @@ class TestStockReconciliation(unittest.TestCase): self.assertEqual(sr.get("items")[0].valuation_rate, 0) self.assertEqual(sr.get("items")[0].amount, 0) + def test_backdated_stock_reco_qty_reposting(self): + """ + Test if a backdated stock reco recalculates future qty until next reco. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] + PR1 | PR | 10 | 18 (posting date: today-3) + PR2 | PR | 1 | 19 (posting date: today-2) + SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] + PR3 | PR | 1 | 7 (posting date: today) # can't post future PR + """ + item_code = "Backdated-Reco-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, + posting_date=add_days(nowdate(), -3)) + pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=add_days(nowdate(), -2)) + pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=nowdate()) + + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(pr3_balance, 12) + + # post backdated stock reco in between + sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100, + posting_date=add_days(nowdate(), -1)) + pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr3_balance, 7) + + # post backdated stock reco at the start + sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100, + posting_date=add_days(nowdate(), -4)) + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, + "qty_after_transaction") + sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 18) + self.assertEqual(pr2_balance, 19) + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + + # cancel backdated stock reco and check future impact + sr5.cancel() + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, + "qty_after_transaction") + sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(pr2_balance, 11) + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + + # teardown + sr4.cancel() + pr3.cancel() + pr2.cancel() + pr1.cancel() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From 44c1e8da06e1c725f715120e15d6500ca0044eca Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 21:56:03 +0530 Subject: [PATCH 347/550] chore: Test to block backdated reco causing future scarcity --- .../test_stock_reconciliation.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index f7b243221a..7b98c7b3e2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -273,6 +273,49 @@ class TestStockReconciliation(unittest.TestCase): pr2.cancel() pr1.cancel() + def test_backdated_stock_reco_future_negative_stock(self): + """ + Test if a backdated stock reco causes future negative stock and is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + PR1 | PR | 10 | 10 (posting date: today-2) + SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] + DN2 | DN | -2 | 8(-1) (posting date: today) + """ + from erpnext.stock.stock_ledger import NegativeStockError + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = "Backdated-Reco-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0) + + pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, + posting_date=add_days(nowdate(), -2)) + dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120, + posting_date=nowdate()) + + pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, + "qty_after_transaction") + dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(pr1_balance, 10) + self.assertEqual(dn2_balance, 8) + + # check if stock reco is blocked + sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100, + posting_date=add_days(nowdate(), -1), do_not_submit=True) + self.assertRaises(NegativeStockError, sr3.submit) + + # teardown + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting) + sr3.cancel() + dn2.cancel() + pr1.cancel() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From c21d3d486597a3a88a98fb34c8616bb6117a6499 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 22:47:59 +0530 Subject: [PATCH 348/550] fix: Debug tests --- .../service_level_agreement/test_service_level_agreement.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4ef4930659..61666f142a 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -103,6 +103,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() + frappe.clear_cache(doctype=doctype) meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: From 32436eceda9d976692aa6a48f75738d77a00af9a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 23:34:09 +0530 Subject: [PATCH 349/550] fix: Debug tests --- .../service_level_agreement/test_service_level_agreement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 61666f142a..e5076193e2 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -103,7 +103,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() - frappe.clear_cache(doctype=doctype) + frappe.clear_cache(doctype=doctype.doctype) meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: From 66c04ca9841a6b30e602a2a29dfd0fa45f3ea3e6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Jul 2021 10:56:42 +0530 Subject: [PATCH 350/550] fix: Debug tests --- .../service_level_agreement/test_service_level_agreement.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index e5076193e2..4ef4930659 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -103,7 +103,6 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() - frappe.clear_cache(doctype=doctype.doctype) meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: From 96eb3be6df20c4d3ac38066ed12e3c1e7d133e3a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Jul 2021 11:21:44 +0530 Subject: [PATCH 351/550] fix: Debug Tests --- .../service_level_agreement/test_service_level_agreement.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4ef4930659..a5481b1123 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -220,9 +220,9 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') - # def tearDown(self): - # for d in frappe.get_all("Service Level Agreement"): - # frappe.delete_doc("Service Level Agreement", d.name, force=1) + def tearDown(self): + for d in frappe.get_all("Service Level Agreement"): + frappe.delete_doc("Service Level Agreement", d.name, force=1) def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None, doctype="Issue"): From f67c95d32feb9e88218fae003b21471dc5c4662e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 6 Jul 2021 13:27:48 +0530 Subject: [PATCH 352/550] fix: flaky SLA test --- .../service_level_agreement/test_service_level_agreement.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 2a8446d29f..0d20b98fa7 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -81,10 +81,9 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA custom fields created for leads sla_fields = get_service_level_agreement_fields() - meta = frappe.get_meta(doctype, cached=False) for field in sla_fields: - self.assertTrue(meta.has_field(field.get("fieldname"))) + self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")})) def test_docfield_creation_for_sla_on_custom_dt(self): doctype = create_custom_doctype() @@ -102,10 +101,9 @@ class TestServiceLevelAgreement(unittest.TestCase): # check SLA docfields created sla_fields = get_service_level_agreement_fields() - meta = frappe.get_meta(doctype.name, cached=False) for field in sla_fields: - self.assertTrue(meta.has_field(field.get("fieldname"))) + self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name})) def test_sla_application(self): # Default Service Level Agreement From 842674ce79c8f2130d60b90b05afd9feb9c05bb0 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Tue, 6 Jul 2021 13:34:32 +0530 Subject: [PATCH 353/550] fix: Added a message to enable appontment booking if disabled (#26334) --- erpnext/www/book_appointment/index.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 7bfac89f30..ccfa97bc62 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -2,6 +2,7 @@ import frappe import datetime import json import pytz +from frappe import _ WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -14,7 +15,8 @@ def get_context(context): if is_enabled: return context else: - frappe.local.flags.redirect_location = '/404' + frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"), + http_status_code=302, indicator_color="red") raise frappe.Redirect @frappe.whitelist(allow_guest=True) From 1f5e2ba8e773ccb8ee37f0ec28096604fc8b3f31 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 6 Jul 2021 13:36:23 +0530 Subject: [PATCH 354/550] fix: payroll-entry minor fix --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 36e728fc99..388a44d895 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -686,7 +686,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) - emp = filters.get('employees') + emp = filters.get('employees') or [] include_employees = [employee.employee for employee in employee_list if employee.employee not in emp] filters.pop('start_date') filters.pop('end_date') From f0b62f70d5bfdfe1d29d286122368d0d988cafc4 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 6 Jul 2021 13:36:23 +0530 Subject: [PATCH 355/550] fix: payroll-entry minor fix --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 36e728fc99..388a44d895 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -686,7 +686,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) - emp = filters.get('employees') + emp = filters.get('employees') or [] include_employees = [employee.employee for employee in employee_list if employee.employee not in emp] filters.pop('start_date') filters.pop('end_date') From 53e3435770de83ad6d66b00270d23a9eeaaa91a1 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 6 Jul 2021 14:37:21 +0530 Subject: [PATCH 356/550] fix: remove cancelled entries in consolidated financial statements (#26330) --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 7793af737f..56a67bb098 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, acc.account_name, acc.account_number - from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s + from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions), { From c8eca8a448de4e4654c16141fc61f047d563cf55 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 6 Jul 2021 14:37:57 +0530 Subject: [PATCH 357/550] fix: remove cancelled entries in consolidated financial statements (#26331) --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 7793af737f..56a67bb098 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -380,7 +380,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, acc.account_name, acc.account_number - from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s + from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions), { From 8985231a96a967683c3c3d3baad66aedb44757ca Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Jul 2021 15:34:47 +0530 Subject: [PATCH 358/550] fix: Rewrite tests --- .../purchase_invoice/test_purchase_invoice.py | 37 +++++++++---------- .../purchase_receipt/test_purchase_receipt.py | 32 ++++++++-------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2ce65c13bf..189260a29d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -231,25 +231,25 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_purchase_invoice_with_exchange_rate_difference(self): - pr = make_purchase_receipt(currency = "USD", conversion_rate = 70) - pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True") + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice - pi.items[0].purchase_receipt = pr.name - pi.items[0].pr_detail = pr.items[0].name + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', + currency = "USD", conversion_rate = 70) + + pi = create_purchase_invoice(pr.name) + pi.conversion_rate = 80 pi.insert() pi.submit() - # fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account - gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'}) - voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') + # Get exchnage gain and loss account + exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account') - self.assertEqual(pi.name, voucher_no) - - exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + # fetching the latest GL Entry with exchange gain and loss account account + amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit') discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) - self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) @@ -1031,22 +1031,21 @@ class TestPurchaseInvoice(unittest.TestCase): # Check GLE for Purchase Invoice # Zero net effect on final TDS Payable on invoice expected_gle = [ - ['_Test Account Cost for Goods Sold - _TC', 30000, 0], - ['_Test Account Excise Duty - _TC', 0, 3000], - ['Creditors - _TC', 0, 27000], - ['TDS Payable - _TC', 3000, 3000] + ['_Test Account Cost for Goods Sold - _TC', 30000], + ['_Test Account Excise Duty - _TC', -3000], + ['Creditors - _TC', -27000], + ['TDS Payable - _TC', 0] ] - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s + group by account order by account asc""", (purchase_invoice.name), as_dict=1) - print(gl_entries) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) - self.assertEqual(expected_gle[i][1], gle.debit) - self.assertEqual(expected_gle[i][2], gle.credit) + self.assertEqual(expected_gle[i][1], gle.amount) def update_tax_witholding_category(company, account, date): from erpnext.accounts.utils import get_fiscal_year diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d56822a308..dbba21fde1 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1054,30 +1054,30 @@ class TestPurchaseReceipt(unittest.TestCase): def test_purchase_receipt_with_exchange_rate_difference(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice - - pi = create_purchase_invoice(currency = "USD", conversion_rate = 70) - - create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", - properties={"account": '_Test Account Stock In Hand - TCP1'}) + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt - pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1', - company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80, - do_not_save = "True") - + pi = create_purchase_invoice(company="_Test Company with perpetual inventory", + cost_center = "Main - TCP1", + warehouse = "Stores - TCP1", + expense_account ="_Test Account Cost for Goods Sold - TCP1", + currency = "USD", conversion_rate = 70) + + pr = create_purchase_receipt(pi.name) + pr.conversion_rate = 80 pr.items[0].purchase_invoice = pi.name pr.items[0].purchase_invoice_item = pi.items[0].name - pr.insert() + pr.save() pr.submit() - # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account - gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'}) - voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no') - self.assertEqual(pr.name, voucher_no) + # Get exchnage gain and loss account + exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account') - exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit') + # fetching the latest GL Entry with exchange gain and loss account account + amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit') discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount) - self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff) + + self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference From 0734901a89d01c4358fa579517a572705a360dc4 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 6 Jul 2021 15:56:10 +0530 Subject: [PATCH 359/550] fix: stock_rbnb not defined (#26345) --- .../stock/doctype/purchase_receipt/purchase_receipt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 5ba9c7057b..41800e3715 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -291,7 +291,7 @@ class PurchaseReceipt(BuyingController): continue self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks, - stock_rbnb, account_currency=warehouse_account_currency, item=d) + stock_rbnb, account_currency=warehouse_account_currency, item=d) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation @@ -318,11 +318,11 @@ class PurchaseReceipt(BuyingController): (exchange_rate_map[d.purchase_invoice] - self.conversion_rate) self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=credit_currency, item=d) - self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, - remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, + self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0, + remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference, account_currency=credit_currency, item=d) # Amount added through landed-cos-voucher @@ -407,6 +407,7 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked + stock_rbnb = self.get_company_default("stock_received_but_not_billed") i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): From 7f794cc0ea062ed8c3a799400cd9d2b044a164af Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 6 Jul 2021 17:58:44 +0530 Subject: [PATCH 360/550] fix: Purchase Invoice advance test case --- .../purchase_invoice/test_purchase_invoice.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2f5d36c8fa..311745d3cd 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1010,21 +1010,21 @@ class TestPurchaseInvoice(unittest.TestCase): # Check GLE for Purchase Invoice # Zero net effect on final TDS Payable on invoice expected_gle = [ - ['_Test Account Cost for Goods Sold - _TC', 30000, 0], - ['_Test Account Excise Duty - _TC', 0, 3000], - ['Creditors - _TC', 0, 27000], - ['TDS Payable - _TC', 3000, 3000] + ['_Test Account Cost for Goods Sold - _TC', 30000], + ['_Test Account Excise Duty - _TC', -3000], + ['Creditors - _TC', -27000], + ['TDS Payable - _TC', 0] ] - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s + group by account order by account asc""", (purchase_invoice.name), as_dict=1) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) - self.assertEqual(expected_gle[i][1], gle.debit) - self.assertEqual(expected_gle[i][2], gle.credit) + self.assertEqual(expected_gle[i][1], gle.amount) def update_tax_witholding_category(company, account, date): from erpnext.accounts.utils import get_fiscal_year From 5e99aa7f65ea424159b6122b50064023427f89c8 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 6 Jul 2021 18:00:35 +0530 Subject: [PATCH 361/550] fix: stock_rbnb not defined (#26354) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e488b695b5..82c87a83a5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -386,6 +386,7 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked + stock_rbnb = self.get_company_default("stock_received_but_not_billed") i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): From f9e9d86955f20c58f02abf8f3c85c19c014ee28f Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 6 Jul 2021 18:09:21 +0530 Subject: [PATCH 362/550] test: fetching of previous sle (#26352) --- .../test_stock_ledger_entry.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index ba31ad7b06..af2ada8c9a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -54,7 +54,7 @@ class TestStockLedgerEntry(unittest.TestCase): ) # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020 - make_stock_entry( + se = make_stock_entry( item_code="_Test Item for Reposting", source="Stores - _TC", target="Finished Goods - _TC", @@ -64,29 +64,29 @@ class TestStockLedgerEntry(unittest.TestCase): posting_date='2020-04-30', posting_time='14:00' ) - target_wh_sle = get_previous_sle({ + target_wh_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-04-30', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": se.name + }, ["valuation_rate"], as_dict=1) self.assertEqual(target_wh_sle.get("valuation_rate"), 150) # Repack entry on 5-5-2020 repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00') - finished_item_sle = get_previous_sle({ + finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Finished Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-05-05', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": repack.name + }, ["incoming_rate", "valuation_rate"], as_dict=1) self.assertEqual(finished_item_sle.get("incoming_rate"), 540) self.assertEqual(finished_item_sle.get("valuation_rate"), 540) # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150 - create_stock_reconciliation( + sr = create_stock_reconciliation( item_code="_Test Item for Reposting", warehouse="Stores - _TC", qty=50, @@ -109,12 +109,12 @@ class TestStockLedgerEntry(unittest.TestCase): self.assertEqual(target_wh_sle.get("valuation_rate"), 175) # Check valuation rate of repacked item after back-dated entry at Stores - finished_item_sle = get_previous_sle({ + finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Finished Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-05-05', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": repack.name + }, ["incoming_rate", "valuation_rate"], as_dict=1) self.assertEqual(finished_item_sle.get("incoming_rate"), 790) self.assertEqual(finished_item_sle.get("valuation_rate"), 790) From 422325bb74ff39f6e4ebe7367d57ecf3622d56b2 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 6 Jul 2021 18:09:21 +0530 Subject: [PATCH 363/550] test: fetching of previous sle (#26352) --- .../test_stock_ledger_entry.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index ba31ad7b06..af2ada8c9a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -54,7 +54,7 @@ class TestStockLedgerEntry(unittest.TestCase): ) # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020 - make_stock_entry( + se = make_stock_entry( item_code="_Test Item for Reposting", source="Stores - _TC", target="Finished Goods - _TC", @@ -64,29 +64,29 @@ class TestStockLedgerEntry(unittest.TestCase): posting_date='2020-04-30', posting_time='14:00' ) - target_wh_sle = get_previous_sle({ + target_wh_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-04-30', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": se.name + }, ["valuation_rate"], as_dict=1) self.assertEqual(target_wh_sle.get("valuation_rate"), 150) # Repack entry on 5-5-2020 repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00') - finished_item_sle = get_previous_sle({ + finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Finished Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-05-05', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": repack.name + }, ["incoming_rate", "valuation_rate"], as_dict=1) self.assertEqual(finished_item_sle.get("incoming_rate"), 540) self.assertEqual(finished_item_sle.get("valuation_rate"), 540) # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150 - create_stock_reconciliation( + sr = create_stock_reconciliation( item_code="_Test Item for Reposting", warehouse="Stores - _TC", qty=50, @@ -109,12 +109,12 @@ class TestStockLedgerEntry(unittest.TestCase): self.assertEqual(target_wh_sle.get("valuation_rate"), 175) # Check valuation rate of repacked item after back-dated entry at Stores - finished_item_sle = get_previous_sle({ + finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { "item_code": "_Test Finished Item for Reposting", "warehouse": "Finished Goods - _TC", - "posting_date": '2020-05-05', - "posting_time": '14:00' - }) + "voucher_type": "Stock Entry", + "voucher_no": repack.name + }, ["incoming_rate", "valuation_rate"], as_dict=1) self.assertEqual(finished_item_sle.get("incoming_rate"), 790) self.assertEqual(finished_item_sle.get("valuation_rate"), 790) From ca87745be1e8ba7dad7ce2e49a30ff6b7d2e0976 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 6 Jul 2021 23:43:08 +0530 Subject: [PATCH 364/550] fix: Rename get_gl_entries_on_asset_movement to get_gl_entries_on_asset_disposal_and_regain --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 +++--- erpnext/assets/doctype/asset/depreciation.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index dc5282037a..6a3db939cf 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.assets.doctype.asset.depreciation \ - import get_disposal_account_and_cost_center, get_gl_entries_on_asset_movement + import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal_and_regain from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no from erpnext.setup.doctype.company.company import update_company_current_month_sales @@ -930,11 +930,11 @@ class SalesInvoice(SellingController): .format(item.item_code, item.idx)) if self.is_return: - fixed_asset_gl_entries = get_gl_entries_on_asset_movement(asset, + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal_and_regain(asset, item.base_net_amount, item.finance_book, True) asset.db_set("disposal_date", None) else: - fixed_asset_gl_entries = get_gl_entries_on_asset_movement(asset, + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal_and_regain(asset, item.base_net_amount, item.finance_book) asset.db_set("disposal_date", self.posting_date) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a18f4278e1..1d877a86f6 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -147,7 +147,7 @@ def scrap_asset(asset_name): je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) - for entry in get_gl_entries_on_asset_movement(asset): + for entry in get_gl_entries_on_asset_disposal_and_regain(asset): entry.update({ "reference_type": "Asset", "reference_name": asset_name @@ -177,7 +177,7 @@ def restore_asset(asset_name): asset.set_status() @frappe.whitelist() -def get_gl_entries_on_asset_movement(asset, selling_amount=0, finance_book=None, is_return = False): +def get_gl_entries_on_asset_disposal_and_regain(asset, selling_amount=0, finance_book=None, is_return = False): fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center From 0bd190b88592faeae6783fa7cfd9f9b70bd93fb4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Jul 2021 14:24:42 +0530 Subject: [PATCH 365/550] fix: stock entry with putaway rule not working --- erpnext/stock/doctype/putaway_rule/putaway_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index ea26caced0..0f50bcd6ea 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -97,7 +97,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: - warehouse = source_warehouse or item.warehouse + warehouse = source_warehouse or item.get('warehouse') if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) From dd1b2995a88a7379af52d6a3d27d4f676ebc6777 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Jul 2021 14:24:42 +0530 Subject: [PATCH 366/550] fix: stock entry with putaway rule not working --- erpnext/stock/doctype/putaway_rule/putaway_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index ea26caced0..0f50bcd6ea 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -97,7 +97,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) if not rules: - warehouse = source_warehouse or item.warehouse + warehouse = source_warehouse or item.get('warehouse') if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) From a01264dae77f6f54bb4a099dfda295afca2b47de Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 6 Jul 2021 18:00:35 +0530 Subject: [PATCH 367/550] fix: stock_rbnb not defined (#26354) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e488b695b5..82c87a83a5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -386,6 +386,7 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked + stock_rbnb = self.get_company_default("stock_received_but_not_billed") i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): From 290350c86f3fdb5845312986d9c55c1d62c0939c Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 7 Jul 2021 12:10:02 +0530 Subject: [PATCH 368/550] chore: add product listing link in settings (#26026) * chore: add product listing link in settings * chore: add icon in workspace card Co-authored-by: Ankush --- .../workspace/erpnext_settings/erpnext_settings.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 014f4095c1..6ca3d637da 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -11,10 +11,11 @@ "hide_custom": 0, "icon": "settings", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "ERPNext Settings", "links": [], - "modified": "2020-12-01 13:38:37.759596", + "modified": "2021-06-12 01:58:11.399566", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -109,6 +110,13 @@ "label": "Domain Settings", "link_to": "Domain Settings", "type": "DocType" + }, + { + "doc_view": "", + "icon": "retail", + "label": "Products Settings", + "link_to": "Products Settings", + "type": "DocType" } ] -} \ No newline at end of file +} From 3a7f25b218c4522255ee27e37881147cc2900289 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 7 Jul 2021 15:59:52 +0530 Subject: [PATCH 369/550] fix: Make functions more readable --- .../doctype/sales_invoice/sales_invoice.py | 8 +- erpnext/assets/doctype/asset/depreciation.py | 81 ++++++++++--------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6a3db939cf..8ef5bcb4d7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.assets.doctype.asset.depreciation \ - import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal_and_regain + import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no from erpnext.setup.doctype.company.company import update_company_current_month_sales @@ -930,11 +930,11 @@ class SalesInvoice(SellingController): .format(item.item_code, item.idx)) if self.is_return: - fixed_asset_gl_entries = get_gl_entries_on_asset_disposal_and_regain(asset, - item.base_net_amount, item.finance_book, True) + fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset, + item.base_net_amount, item.finance_book) asset.db_set("disposal_date", None) else: - fixed_asset_gl_entries = get_gl_entries_on_asset_disposal_and_regain(asset, + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, item.base_net_amount, item.finance_book) asset.db_set("disposal_date", self.posting_date) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 1d877a86f6..8fdbbf95d4 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -147,7 +147,7 @@ def scrap_asset(asset_name): je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) - for entry in get_gl_entries_on_asset_disposal_and_regain(asset): + for entry in get_gl_entries_on_asset_disposal(asset): entry.update({ "reference_type": "Asset", "reference_name": asset_name @@ -176,43 +176,10 @@ def restore_asset(asset_name): asset.set_status() -@frappe.whitelist() -def get_gl_entries_on_asset_disposal_and_regain(asset, selling_amount=0, finance_book=None, is_return = False): - fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset) - disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) - depreciation_cost_center = asset.cost_center or depreciation_cost_center +def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): + fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \ + get_asset_details(asset, finance_book) - idx = 1 - if finance_book: - for d in asset.finance_books: - if d.finance_book == finance_book: - idx = d.idx - break - - value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation - if asset.calculate_depreciation else asset.value_after_depreciation) - accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) - - if is_return: - gl_entries = get_gl_entries_on_asset_regain(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount) - profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) - - else: - gl_entries = get_gl_entries_on_asset_disposal(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount) - profit_amount = flt(selling_amount) - flt(value_after_depreciation) - - if profit_amount: - debit_or_credit = "debit" if profit_amount < 0 else "credit" - gl_entries.append({ - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount) - }) - - return gl_entries - -def get_gl_entries_on_asset_regain(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount): gl_entries = [ { "account": fixed_asset_account, @@ -228,9 +195,16 @@ def get_gl_entries_on_asset_regain(fixed_asset_account, asset, depreciation_cost } ] + profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) + if profit_amount: + get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + return gl_entries -def get_gl_entries_on_asset_disposal(fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount): +def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): + fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation = \ + get_asset_details(asset, finance_book) + gl_entries = [ { "account": fixed_asset_account, @@ -246,8 +220,39 @@ def get_gl_entries_on_asset_disposal(fixed_asset_account, asset, depreciation_co } ] + profit_amount = flt(selling_amount) - flt(value_after_depreciation) + if profit_amount: + get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + return gl_entries +def get_asset_details(asset, finance_book=None): + fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset) + disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) + depreciation_cost_center = asset.cost_center or depreciation_cost_center + + idx = 1 + if finance_book: + for d in asset.finance_books: + if d.finance_book == finance_book: + idx = d.idx + break + + value_after_depreciation = (asset.finance_books[idx - 1].value_after_depreciation + if asset.calculate_depreciation else asset.value_after_depreciation) + accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) + + return fixed_asset_account, asset, depreciation_cost_center, accumulated_depr_account, accumulated_depr_amount, disposal_account, value_after_depreciation + +def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): + debit_or_credit = "debit" if profit_amount < 0 else "credit" + gl_entries.append({ + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount) + }) + @frappe.whitelist() def get_disposal_account_and_cost_center(company): disposal_account, depreciation_cost_center = frappe.get_cached_value('Company', company, From 00f90c50c0577d86c530197810dfca28c683345b Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 7 Jul 2021 12:10:02 +0530 Subject: [PATCH 370/550] chore: add product listing link in settings (#26026) * chore: add product listing link in settings * chore: add icon in workspace card Co-authored-by: Ankush --- .../workspace/erpnext_settings/erpnext_settings.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 014f4095c1..6ca3d637da 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -11,10 +11,11 @@ "hide_custom": 0, "icon": "settings", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "ERPNext Settings", "links": [], - "modified": "2020-12-01 13:38:37.759596", + "modified": "2021-06-12 01:58:11.399566", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -109,6 +110,13 @@ "label": "Domain Settings", "link_to": "Domain Settings", "type": "DocType" + }, + { + "doc_view": "", + "icon": "retail", + "label": "Products Settings", + "link_to": "Products Settings", + "type": "DocType" } ] -} \ No newline at end of file +} From 298b43c177761052eef9beb1864eed666cae71c6 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 7 Jul 2021 20:56:15 +0530 Subject: [PATCH 371/550] fix: Let create_item() make items that are fixed assets --- erpnext/stock/doctype/item/test_item.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index c7467a5a0f..922049f144 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -587,8 +587,8 @@ def make_item_variant(): test_records = frappe.get_test_records('Item') def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC", - is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, - company="_Test Company"): + is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, is_fixed_asset=0, + asset_category=None, company="_Test Company"): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") item.item_code = item_code @@ -596,6 +596,8 @@ def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test W item.description = item_code item.item_group = "All Item Groups" item.is_stock_item = is_stock_item + item.is_fixed_asset = is_fixed_asset + item.asset_category = asset_category item.opening_stock = opening_stock item.valuation_rate = valuation_rate item.is_purchase_item = is_purchase_item From cfff3b87266d92baafeabb082c8c77a3b83efbe3 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 7 Jul 2021 21:04:20 +0530 Subject: [PATCH 372/550] fix: Test GL Entries made when an Asset is returned --- .../sales_invoice/test_sales_invoice.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 114b7d2d35..ee8b057b6d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -10,6 +10,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile +from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_category from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from frappe.model.naming import make_autoname @@ -1069,6 +1070,36 @@ class TestSalesInvoice(unittest.TestCase): self.assertFalse(si1.outstanding_amount) self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + def test_gle_made_when_asset_is_returned(self): + create_item(item_code="_Test Item linked with Asset", is_stock_item = 0, is_fixed_asset=1, asset_category="Computers") + asset = create_asset(item_code="_Test Item linked with Asset") + + si = create_sales_invoice(item_code="_Test Item linked with Asset", asset=asset.name, qty=1, rate=90000) + return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="_Test Item linked with Asset", asset=asset.name, qty=-1, rate=90000) + + disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account") + + # Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000 + loss_for_si = frappe.get_all( + "GL Entry", + filters = { + "voucher_no": si.name, + "account": disposal_account + }, + fields = ["credit", "debit"] + )[0] + + loss_for_return_si = frappe.get_all( + "GL Entry", + filters = { + "voucher_no": return_si.name, + "account": disposal_account + }, + fields = ["credit", "debit"] + )[0] + + self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit']) + self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit']) def test_discount_on_net_total(self): si = frappe.copy_doc(test_records[2]) @@ -2164,6 +2195,7 @@ def create_sales_invoice(**args): "rate": args.rate if args.get("rate") is not None else 100, "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "asset": args.asset or None, "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, "conversion_factor": 1 From eaef371585324c619e4f2a3a25f245409a779fee Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 8 Jul 2021 10:52:41 +0530 Subject: [PATCH 373/550] fix: escape quotes while fetching customer emails (#26329) --- .../process_statement_of_accounts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 0b0ee904ff..500952e38a 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): @frappe.whitelist() def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): billing_email = frappe.db.sql(""" - SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ - WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ - c.is_billing_contact=1 \ - order by c.creation desc""") + SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent + WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1 + order by c.creation desc""", customer_name) if len(billing_email) == 0 or (billing_email[0][0] is None): if billing_and_primary: From fac420ee09d0e7870e9ecd98a21628b095497c41 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 8 Jul 2021 13:05:14 +0530 Subject: [PATCH 374/550] fix: Removed un-used flag --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 388a44d895..13cc423fc2 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -117,7 +117,6 @@ class PayrollEntry(Document): Creates salary slip for selected employees if already not created """ self.check_permission('write') - self.created = 1 employees = [emp.employee for emp in self.employees] if employees: args = frappe._dict({ From 8f945a9852281083a0861f2fdb193112fb7bb236 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 8 Jul 2021 13:05:14 +0530 Subject: [PATCH 375/550] fix: Removed un-used flag --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 388a44d895..13cc423fc2 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -117,7 +117,6 @@ class PayrollEntry(Document): Creates salary slip for selected employees if already not created """ self.check_permission('write') - self.created = 1 employees = [emp.employee for emp in self.employees] if employees: args = frappe._dict({ From 091f41e98657839c565d8825a3ffa79659c81d89 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 14:57:54 +0530 Subject: [PATCH 376/550] fix: yet another fix for flaky SLA Test --- .../service_level_agreement.json | 5 +++-- .../test_service_level_agreement.py | 13 ++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) 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 61ca3a334e..de3389aa42 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -150,7 +150,8 @@ "fieldtype": "Link", "label": "Document Type", "options": "DocType", - "reqd": 1 + "reqd": 1, + "set_only_once": 1 }, { "default": "1", @@ -178,7 +179,7 @@ } ], "links": [], - "modified": "2021-05-29 13:35:41.956849", + "modified": "2021-07-08 12:28:46.283334", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 0d20b98fa7..7c18a6577f 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -328,16 +328,11 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "entity": entity }) - service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters) + sla = frappe.db.exists("Service Level Agreement", filters) + if sla: + frappe.delete_doc("Service Level Agreement", sla, force=1) - if not service_level_agreement_exists: - doc = frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) - else: - doc = frappe.get_doc("Service Level Agreement", service_level_agreement_exists) - doc.update(service_level_agreement) - doc.save() - - return doc + return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True) def create_customer(): From b6ff8917e819e8fb272f4cd91e4ee01217027889 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 8 Jul 2021 17:25:37 +0530 Subject: [PATCH 377/550] fix: query for training Event --- erpnext/hr/doctype/training_event/training_event.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index 064dfb2455..d5f6e5f573 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -33,7 +33,8 @@ frappe.ui.form.on('Training Event', { frm.set_query("employee", "employees", function () { return { filters: { - name: ["NOT IN", emp] + name: ["NOT IN", emp], + status: "Active" } }; }); From a82e9e42e1746d42ecf32b6ff4ee7e3fb7823d62 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 8 Jul 2021 17:25:37 +0530 Subject: [PATCH 378/550] fix: query for training Event --- erpnext/hr/doctype/training_event/training_event.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index b7d34b178a..a20f0b70af 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -34,7 +34,8 @@ frappe.ui.form.on("Training Event Employee", { frm.set_query("employee", "employees", function () { return { filters: { - name: ["NOT IN", emp] + name: ["NOT IN", emp], + status: "Active" } }; }); From 257cbd3b92b8fd78855b9c6ae98c40c5708f5fe7 Mon Sep 17 00:00:00 2001 From: Alan <2.alan.tom@gmail.com> Date: Thu, 8 Jul 2021 18:44:30 +0530 Subject: [PATCH 379/550] fix: track changes on batch (#26382) --- erpnext/stock/doctype/batch/batch.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index e6d2e1330b..fc4cf1dbdb 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -193,7 +193,7 @@ "image_field": "image", "links": [], "max_attachments": 5, - "modified": "2021-01-07 11:10:09.149170", + "modified": "2021-07-08 16:22:01.343105", "modified_by": "Administrator", "module": "Stock", "name": "Batch", @@ -217,5 +217,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "batch_id" + "title_field": "batch_id", + "track_changes": 1 } \ No newline at end of file From 88929b055c3847b386f7ab8e708ee0a6bc1303ec Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 19:27:53 +0530 Subject: [PATCH 380/550] fix: precision for expected values in payment entry test --- .../accounts/doctype/payment_entry/test_payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 4641d6b5ff..d1302f5ae7 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase): party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center) self.assertEqual(pe.cost_center, si.cost_center) - self.assertEqual(expected_account_balance, account_balance) - self.assertEqual(expected_party_balance, party_balance) - self.assertEqual(expected_party_account_balance, party_account_balance) + self.assertEqual(flt(expected_account_balance), account_balance) + self.assertEqual(flt(expected_party_balance), party_balance) + self.assertEqual(flt(expected_party_account_balance), party_account_balance) def create_payment_terms_template(): From 3888488b3628df39c152fb48c3a8b17b3af6cc35 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 8 Jul 2021 19:27:53 +0530 Subject: [PATCH 381/550] fix: precision for expected values in payment entry test --- .../accounts/doctype/payment_entry/test_payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 4641d6b5ff..d1302f5ae7 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -589,9 +589,9 @@ class TestPaymentEntry(unittest.TestCase): party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center) self.assertEqual(pe.cost_center, si.cost_center) - self.assertEqual(expected_account_balance, account_balance) - self.assertEqual(expected_party_balance, party_balance) - self.assertEqual(expected_party_account_balance, party_account_balance) + self.assertEqual(flt(expected_account_balance), account_balance) + self.assertEqual(flt(expected_party_balance), party_balance) + self.assertEqual(flt(expected_party_account_balance), party_account_balance) def create_payment_terms_template(): From 8f3c7ab4029dafb1d5e1c3384eb48d02b50f18a1 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 10:35:55 +0530 Subject: [PATCH 382/550] fix: escape quotes while fetching customer emails (#26329) (#26376) --- .../process_statement_of_accounts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 0b0ee904ff..500952e38a 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -207,10 +207,9 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): @frappe.whitelist() def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): billing_email = frappe.db.sql(""" - SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ - WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ - c.is_billing_contact=1 \ - order by c.creation desc""") + SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent + WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1 + order by c.creation desc""", customer_name) if len(billing_email) == 0 or (billing_email[0][0] is None): if billing_and_primary: From bf462abb00798a73549e71f1113ee9ed8a8b18f0 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 9 Jul 2021 13:06:38 +0530 Subject: [PATCH 383/550] fix: Rename function and tweak logic - Dont validate PI on `else` --- .../landed_cost_voucher.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) 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 1f78867bef..bf969f99f8 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -41,7 +41,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() - self.validate_purchase_receipts() + self.validate_receipt_documents() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -56,25 +56,23 @@ class LandedCostVoucher(Document): frappe.throw(_("Please enter Receipt Document")) - def validate_purchase_receipts(self): + def validate_receipt_documents(self): receipt_documents = [] for d in self.get("purchase_receipts"): - doc_data = frappe.db.get_values( - d.receipt_document_type, - d.receipt_document, - ["docstatus", "update_stock"], - as_dict=1 - )[0] - if doc_data.get("docstatus") != 1: - msg = f"Row {d.idx}: Receipt Document {frappe.bold(d.receipt_document)} must be submitted" + docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") + if docstatus != 1: + msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" frappe.throw(_(msg), title=_("Invalid Document")) - elif d.receipt_document_type == "Purchase Invoice" and not doc_data.get("update_stock"): - msg = _(f"Row {d.idx}: Purchase Invoice {frappe.bold(d.receipt_document)} has no stock impact.") - msg += "
    " + _("Please create Landed Cost Vouchers against Invoices with 'Update Stock' enabled.") - frappe.throw(msg, title=_("Incorrect Invoice")) - else: - receipt_documents.append(d.receipt_document) + + if d.receipt_document_type == "Purchase Invoice": + update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") + if not update_stock: + msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document)) + msg += "
    " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.") + frappe.throw(msg, title=_("Incorrect Invoice")) + + receipt_documents.append(d.receipt_document) for item in self.get("items"): if not item.receipt_document: From 86f5dfbb8ffc7f1340c5d05ecc445b39b8d5892f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Jul 2021 14:24:38 +0530 Subject: [PATCH 384/550] fix: Validate LCV for Invoices without Update Stock --- .../landed_cost_voucher/landed_cost_voucher.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 5df4d8743f..1f78867bef 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -60,8 +60,19 @@ class LandedCostVoucher(Document): receipt_documents = [] for d in self.get("purchase_receipts"): - if frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") != 1: - frappe.throw(_("Receipt document must be submitted")) + doc_data = frappe.db.get_values( + d.receipt_document_type, + d.receipt_document, + ["docstatus", "update_stock"], + as_dict=1 + )[0] + if doc_data.get("docstatus") != 1: + msg = f"Row {d.idx}: Receipt Document {frappe.bold(d.receipt_document)} must be submitted" + frappe.throw(_(msg), title=_("Invalid Document")) + elif d.receipt_document_type == "Purchase Invoice" and not doc_data.get("update_stock"): + msg = _(f"Row {d.idx}: Purchase Invoice {frappe.bold(d.receipt_document)} has no stock impact.") + msg += "
    " + _("Please create Landed Cost Vouchers against Invoices with 'Update Stock' enabled.") + frappe.throw(msg, title=_("Incorrect Invoice")) else: receipt_documents.append(d.receipt_document) From 98173038b5fb7cfad22ee48febdca62d888c70c8 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 9 Jul 2021 13:06:38 +0530 Subject: [PATCH 385/550] fix: Rename function and tweak logic - Dont validate PI on `else` --- .../landed_cost_voucher.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) 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 1f78867bef..bf969f99f8 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -41,7 +41,7 @@ class LandedCostVoucher(Document): def validate(self): self.check_mandatory() - self.validate_purchase_receipts() + self.validate_receipt_documents() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -56,25 +56,23 @@ class LandedCostVoucher(Document): frappe.throw(_("Please enter Receipt Document")) - def validate_purchase_receipts(self): + def validate_receipt_documents(self): receipt_documents = [] for d in self.get("purchase_receipts"): - doc_data = frappe.db.get_values( - d.receipt_document_type, - d.receipt_document, - ["docstatus", "update_stock"], - as_dict=1 - )[0] - if doc_data.get("docstatus") != 1: - msg = f"Row {d.idx}: Receipt Document {frappe.bold(d.receipt_document)} must be submitted" + docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") + if docstatus != 1: + msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" frappe.throw(_(msg), title=_("Invalid Document")) - elif d.receipt_document_type == "Purchase Invoice" and not doc_data.get("update_stock"): - msg = _(f"Row {d.idx}: Purchase Invoice {frappe.bold(d.receipt_document)} has no stock impact.") - msg += "
    " + _("Please create Landed Cost Vouchers against Invoices with 'Update Stock' enabled.") - frappe.throw(msg, title=_("Incorrect Invoice")) - else: - receipt_documents.append(d.receipt_document) + + if d.receipt_document_type == "Purchase Invoice": + update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") + if not update_stock: + msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document)) + msg += "
    " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.") + frappe.throw(msg, title=_("Incorrect Invoice")) + + receipt_documents.append(d.receipt_document) for item in self.get("items"): if not item.receipt_document: From 99f449939df4dcef2ba135d9dfc58438fee00989 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 1 Jul 2021 17:20:24 +0530 Subject: [PATCH 386/550] fix: Order Items by weightage in the web items query --- erpnext/shopping_cart/product_query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 3eab4ffbcc..6c92d967d0 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -87,7 +87,8 @@ class ProductQuery: filters=self.filters, or_filters=self.or_filters, start=start, - limit=self.page_length + limit=self.page_length, + order_by="weightage desc" ) # Combine results having context of website item groups into item results From d53991857c59ab41888e9fe0f28964f346f84ec7 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:33:00 +0530 Subject: [PATCH 387/550] fix: Fixed Budget Variance Graph color from all black to default (#26368) --- .../report/budget_variance_report/budget_variance_report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 9c9ada871c..f1b231b690 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -397,6 +397,7 @@ def get_chart_data(filters, columns, data): {'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} ] - } + }, + 'type' : 'bar' } From 95a15ed85d78f8506d65d6aad20600e489623b40 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:34:51 +0530 Subject: [PATCH 388/550] fix: value fetching for custom field in POS (#26366) --- erpnext/selling/page/point_of_sale/pos_payment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index c484873d3e..f1a166b523 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -56,7 +56,7 @@ erpnext.PointOfSale.Payment = class { ); let df_events = { onchange: function() { - frm.set_value(this.df.fieldname, this.value); + frm.set_value(this.df.fieldname, this.get_value()); } }; if (df.fieldtype == "Button") { From 9ac63da457ec7e168e3a8862bdb0a15dbf149902 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Fri, 9 Jul 2021 14:35:11 +0530 Subject: [PATCH 389/550] fix: value fetching for custom field in POS (#26367) --- erpnext/selling/page/point_of_sale/pos_payment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index c484873d3e..f1a166b523 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -56,7 +56,7 @@ erpnext.PointOfSale.Payment = class { ); let df_events = { onchange: function() { - frm.set_value(this.df.fieldname, this.value); + frm.set_value(this.df.fieldname, this.get_value()); } }; if (df.fieldtype == "Button") { From 47ccfd6c9ddca414ababbdb9222431426d14c806 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 14:36:13 +0530 Subject: [PATCH 390/550] fix: column 'outstanding_amount' cannot be null (#26371) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index adaf99a790..0c21aae944 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1318,9 +1318,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre return frappe._dict({ "due_date": ref_doc.get("due_date"), - "total_amount": total_amount, - "outstanding_amount": outstanding_amount, - "exchange_rate": exchange_rate, + "total_amount": flt(total_amount), + "outstanding_amount": flt(outstanding_amount), + "exchange_rate": flt(exchange_rate), "bill_no": bill_no }) From b1997029775d599b6c1bac3b1ee83edef5edec64 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 14:37:34 +0530 Subject: [PATCH 391/550] fix(e-invoicing): allow export invoice even if no taxes applied (#26363) --- erpnext/regional/india/e_invoice/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5d33c1b100..fad1c4636d 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -42,7 +42,10 @@ def validate_eligibility(doc): invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') - no_taxes_applied = not doc.get('taxes') + + # if export invoice, then taxes can be empty + # invoice can only be ineligible if no taxes applied and is not an export invoice + no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: From 77e00403c82ea0d15caab65e81659dc10de73bed Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 14:41:03 +0530 Subject: [PATCH 392/550] fix: omit item discount amount for e-invoicing (#26353) --- erpnext/regional/india/e_invoice/einvoice.js | 4 +++- erpnext/regional/india/e_invoice/utils.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 23d4fe9030..8ad30fa910 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -1,6 +1,8 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.ui.form.on(doctype, { async refresh(frm) { + if (frm.doc.docstatus == 2) return; + const res = await frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', args: { doc: frm.doc } @@ -111,7 +113,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const action = () => { - let message = __('Cancellation of e-way bill is currently not supported. '); + let message = __('Cancellation of e-way bill is currently not supported.') + ' '; message += '

    '; message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index fad1c4636d..81c7a6b9a0 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -191,9 +191,10 @@ def get_item_list(invoice): item.qty = abs(item.qty) - item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) - item.gross_amount = abs(item.taxable_value) + item.discount_amount + item.unit_rate = abs(item.taxable_value / item.qty) + item.gross_amount = abs(item.taxable_value) item.taxable_value = abs(item.taxable_value) + item.discount_amount = 0 item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None From 8e8434a78a66178652bdb850c582399b51e22382 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 15:32:28 +0530 Subject: [PATCH 393/550] fix: omit item discount amount for e-invoicing (#26353) (#26407) --- erpnext/regional/india/e_invoice/einvoice.js | 4 +++- erpnext/regional/india/e_invoice/utils.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 23d4fe9030..8ad30fa910 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -1,6 +1,8 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.ui.form.on(doctype, { async refresh(frm) { + if (frm.doc.docstatus == 2) return; + const res = await frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', args: { doc: frm.doc } @@ -111,7 +113,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const action = () => { - let message = __('Cancellation of e-way bill is currently not supported. '); + let message = __('Cancellation of e-way bill is currently not supported.') + ' '; message += '

    '; message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 11ebef724c..405b10ff54 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -188,9 +188,10 @@ def get_item_list(invoice): item.qty = abs(item.qty) - item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) - item.gross_amount = abs(item.taxable_value) + item.discount_amount + item.unit_rate = abs(item.taxable_value / item.qty) + item.gross_amount = abs(item.taxable_value) item.taxable_value = abs(item.taxable_value) + item.discount_amount = 0 item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None From fe4f58d0f62fe2b9b05e112da3d8e5f85676fe6f Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 15:32:54 +0530 Subject: [PATCH 394/550] fix(e-invoicing): allow export invoice even if no taxes applied (#26405) --- erpnext/regional/india/e_invoice/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 405b10ff54..ea600d9097 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -42,7 +42,10 @@ def validate_eligibility(doc): invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') - no_taxes_applied = not doc.get('taxes') + + # if export invoice, then taxes can be empty + # invoice can only be ineligible if no taxes applied and is not an export invoice + no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: From 13d70434510f19a422f41ee77f4e3d39f13bde05 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 15:33:14 +0530 Subject: [PATCH 395/550] fix: column 'outstanding_amount' cannot be null (#26404) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index adaf99a790..0c21aae944 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1318,9 +1318,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre return frappe._dict({ "due_date": ref_doc.get("due_date"), - "total_amount": total_amount, - "outstanding_amount": outstanding_amount, - "exchange_rate": exchange_rate, + "total_amount": flt(total_amount), + "outstanding_amount": flt(outstanding_amount), + "exchange_rate": flt(exchange_rate), "bill_no": bill_no }) From 2098684bc084ab4ccd0db7cb08ffeb2399be30c5 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 18:09:26 +0530 Subject: [PATCH 396/550] fix(pos): taxes amount in pos item cart (#26410) --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 7cae0e4797..38508c219b 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -472,12 +472,7 @@ erpnext.PointOfSale.ItemCart = class { const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); - const taxes = frm.doc.taxes.map(t => { - return { - description: t.description, rate: t.rate - }; - }); - this.render_taxes(frm.doc.total_taxes_and_charges, taxes); + this.render_taxes(frm.doc.taxes); } render_net_total(value) { @@ -502,14 +497,14 @@ erpnext.PointOfSale.ItemCart = class { ); } - render_taxes(value, taxes) { + render_taxes(taxes) { if (taxes.length) { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; return `
    ${description}
    -
    ${format_currency(value, currency)}
    +
    ${format_currency(t.tax_amount_after_discount_amount, currency)}
    `; }).join(''); this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); From ef42e80065228d0150fab79f064632104e418db5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 9 Jul 2021 19:23:13 +0530 Subject: [PATCH 397/550] fix: Sider issues --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index b16631b316..31de48216b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -10,7 +10,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile -from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_category +from erpnext.assets.doctype.asset.test_asset import create_asset from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from frappe.model.naming import make_autoname From 446171acc4232ec964473804e73ab6dfc413081d Mon Sep 17 00:00:00 2001 From: Anuja Date: Tue, 29 Jun 2021 21:27:02 +0530 Subject: [PATCH 398/550] fix: updated onboarding steps for selling module --- .../form_tour/sales_order/sales_order.json | 97 +++++++++++++++++++ .../selling_settings/selling_settings.json | 65 +++++++++++++ .../module_onboarding/selling/selling.json | 4 +- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_product/create_a_product.json | 4 +- .../create_a_quotation.json | 2 +- .../create_a_sales_order.json | 21 ++++ .../introduction_to_selling.json | 2 +- .../selling_settings/selling_settings.json | 8 +- .../setup_your_warehouse.json | 4 +- 10 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 erpnext/selling/form_tour/sales_order/sales_order.json create mode 100644 erpnext/selling/form_tour/selling_settings/selling_settings.json create mode 100644 erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json diff --git a/erpnext/selling/form_tour/sales_order/sales_order.json b/erpnext/selling/form_tour/sales_order/sales_order.json new file mode 100644 index 0000000000..a81eb4a043 --- /dev/null +++ b/erpnext/selling/form_tour/sales_order/sales_order.json @@ -0,0 +1,97 @@ +{ + "creation": "2021-06-29 21:13:36.089054", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 21:13:36.089054", + "modified_by": "Administrator", + "module": "Selling", + "name": "Sales Order", + "owner": "Administrator", + "reference_doctype": "Sales Order", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a customer.", + "field": "", + "fieldname": "customer", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Customer", + "next_step_condition": "customer", + "parent_field": "", + "position": "Right", + "title": "Select Customer" + }, + { + "description": "You can add items here.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Items", + "parent_field": "", + "position": "Bottom", + "title": "List of items" + }, + { + "child_doctype": "Sales Order Item", + "description": "Select an item.", + "field": "", + "fieldname": "item_code", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 1, + "label": "Item Code", + "next_step_condition": "eval: doc.item_code", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Select Item" + }, + { + "child_doctype": "Sales Order Item", + "description": "Enter quantity.", + "field": "", + "fieldname": "qty", + "fieldtype": "Float", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Quantity", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Quantity" + }, + { + "child_doctype": "Sales Order Item", + "description": "Enter rate of the item.", + "field": "", + "fieldname": "rate", + "fieldtype": "Currency", + "has_next_condition": 0, + "is_table_field": 1, + "label": "Rate", + "parent_field": "", + "parent_fieldname": "items", + "position": "Right", + "title": "Enter Rate" + }, + { + "description": "You can add sales taxes and charges here.", + "field": "", + "fieldname": "taxes", + "fieldtype": "Table", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Sales Taxes and Charges", + "parent_field": "", + "position": "Bottom", + "title": "Add Sales Taxes and Charges" + } + ], + "title": "Sales Order" +} \ No newline at end of file diff --git a/erpnext/selling/form_tour/selling_settings/selling_settings.json b/erpnext/selling/form_tour/selling_settings/selling_settings.json new file mode 100644 index 0000000000..20c718f8c0 --- /dev/null +++ b/erpnext/selling/form_tour/selling_settings/selling_settings.json @@ -0,0 +1,65 @@ +{ + "creation": "2021-06-29 20:39:19.408763", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-06-29 20:49:01.359489", + "modified_by": "Administrator", + "module": "Selling", + "name": "Selling Settings", + "owner": "Administrator", + "reference_doctype": "Selling Settings", + "save_on_complete": 0, + "steps": [ + { + "description": "By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a Naming Series. Choose the 'Naming Series' option.", + "field": "", + "fieldname": "cust_master_name", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Customer Naming By", + "parent_field": "", + "position": "Right", + "title": "Customer Naming By" + }, + { + "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.", + "field": "", + "fieldname": "so_required", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", + "parent_field": "", + "position": "Left", + "title": "Sales Order Required for Sales Invoice & Delivery Note Creation" + }, + { + "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.", + "field": "", + "fieldname": "dn_required", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Is Delivery Note Required for Sales Invoice Creation?", + "parent_field": "", + "position": "Left", + "title": "Delivery Note Required for Sales Invoice Creation" + }, + { + "description": "Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.", + "field": "", + "fieldname": "selling_price_list", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Price List", + "parent_field": "", + "position": "Right", + "title": "Default Selling Price List" + } + ], + "title": "Selling Settings" +} \ No newline at end of file diff --git a/erpnext/selling/module_onboarding/selling/selling.json b/erpnext/selling/module_onboarding/selling/selling.json index 160208ff68..29f51cbbc6 100644 --- a/erpnext/selling/module_onboarding/selling/selling.json +++ b/erpnext/selling/module_onboarding/selling/selling.json @@ -19,7 +19,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/selling", "idx": 0, "is_complete": 0, - "modified": "2020-07-08 14:05:37.669753", + "modified": "2021-06-29 21:23:24.510122", "modified_by": "Administrator", "module": "Selling", "name": "Selling", @@ -41,7 +41,7 @@ "step": "Create a Quotation" }, { - "step": "Create your first Sales Order" + "step": "Create a Sales Order" }, { "step": "Selling Settings" diff --git a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json index 5a403b06cf..64defbfe3d 100644 --- a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:16:19.731719", @@ -13,6 +12,7 @@ "name": "Create a Customer", "owner": "Administrator", "reference_document": "Customer", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Customer", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json index d2068e167b..52a5861551 100644 --- a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json +++ b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-05-12 18:30:02.489949", + "modified": "2020-10-14 14:53:00.133574", "modified_by": "Administrator", "name": "Create a Product", "owner": "Administrator", "reference_document": "Item", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Product", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json index 27253d15b6..6f1837e24c 100644 --- a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json @@ -5,7 +5,6 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:34:58.958641", @@ -13,6 +12,7 @@ "name": "Create a Quotation", "owner": "Administrator", "reference_document": "Quotation", + "show_form_tour": 0, "show_full_form": 1, "title": "Create a Quotation", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json new file mode 100644 index 0000000000..378f295f0c --- /dev/null +++ b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json @@ -0,0 +1,21 @@ +{ + "action": "Create Entry", + "action_label": "Create Sales Order", + "creation": "2021-06-29 21:22:54.204880", + "description": "A Sales Order is a confirmation of an order from your customer. It is also referred to as Proforma Invoice.\n\nSales Order at the heart of your sales and purchase transactions. Sales Orders are linked in Delivery Note, Sales Invoices, Material Request, and Maintenance transactions. Through Sales Order, you can track fulfillment of the overall deal towards the customer.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-06-29 21:22:54.204880", + "modified_by": "Administrator", + "name": "Create a Sales Order", + "owner": "Administrator", + "reference_document": "Sales Order", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create a Sales Order", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json index d21c1f4954..636453363b 100644 --- a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json +++ b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json @@ -5,13 +5,13 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:29:13.703177", "modified_by": "Administrator", "name": "Introduction to Selling", "owner": "Administrator", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Selling", "validate_action": 1, diff --git a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json index 7996d7b159..af95aa756f 100644 --- a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json +++ b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json @@ -1,19 +1,21 @@ { "action": "Show Form Tour", + "action_label": "Let\u2019s walk-through Selling Settings", "creation": "2020-06-01 13:01:45.615189", + "description": "CRM and Selling module\u2019s features are configurable as per your business needs. Selling Settings is the place where you can set your preferences for:\n1. Customer naming and default values\n2. Billing and shipping preference in sales transactions\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2020-06-01 13:04:14.980743", + "modified": "2021-06-29 20:52:03.640254", "modified_by": "Administrator", "name": "Selling Settings", "owner": "Administrator", "reference_document": "Selling Settings", + "show_form_tour": 0, "show_full_form": 0, - "title": "Configure Selling Settings.", + "title": "Selling Settings", "validate_action": 0 } \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 9457deee26..1e20eb0eba 100644 --- a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-07-04 12:33:16.970031", + "modified": "2020-10-14 14:53:25.538900", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", + "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 From f4d45d60f46d596931093482e65b537c90a58559 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Fri, 9 Jul 2021 20:45:43 +0530 Subject: [PATCH 399/550] Revert "fix: updated onboarding steps for selling module" (#26414) This reverts commit 446171acc4232ec964473804e73ab6dfc413081d. --- .../form_tour/sales_order/sales_order.json | 97 ------------------- .../selling_settings/selling_settings.json | 65 ------------- .../module_onboarding/selling/selling.json | 4 +- .../create_a_customer/create_a_customer.json | 2 +- .../create_a_product/create_a_product.json | 4 +- .../create_a_quotation.json | 2 +- .../create_a_sales_order.json | 21 ---- .../introduction_to_selling.json | 2 +- .../selling_settings/selling_settings.json | 8 +- .../setup_your_warehouse.json | 4 +- 10 files changed, 12 insertions(+), 197 deletions(-) delete mode 100644 erpnext/selling/form_tour/sales_order/sales_order.json delete mode 100644 erpnext/selling/form_tour/selling_settings/selling_settings.json delete mode 100644 erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json diff --git a/erpnext/selling/form_tour/sales_order/sales_order.json b/erpnext/selling/form_tour/sales_order/sales_order.json deleted file mode 100644 index a81eb4a043..0000000000 --- a/erpnext/selling/form_tour/sales_order/sales_order.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "creation": "2021-06-29 21:13:36.089054", - "docstatus": 0, - "doctype": "Form Tour", - "idx": 0, - "is_standard": 1, - "modified": "2021-06-29 21:13:36.089054", - "modified_by": "Administrator", - "module": "Selling", - "name": "Sales Order", - "owner": "Administrator", - "reference_doctype": "Sales Order", - "save_on_complete": 1, - "steps": [ - { - "description": "Select a customer.", - "field": "", - "fieldname": "customer", - "fieldtype": "Link", - "has_next_condition": 1, - "is_table_field": 0, - "label": "Customer", - "next_step_condition": "customer", - "parent_field": "", - "position": "Right", - "title": "Select Customer" - }, - { - "description": "You can add items here.", - "field": "", - "fieldname": "items", - "fieldtype": "Table", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Items", - "parent_field": "", - "position": "Bottom", - "title": "List of items" - }, - { - "child_doctype": "Sales Order Item", - "description": "Select an item.", - "field": "", - "fieldname": "item_code", - "fieldtype": "Link", - "has_next_condition": 1, - "is_table_field": 1, - "label": "Item Code", - "next_step_condition": "eval: doc.item_code", - "parent_field": "", - "parent_fieldname": "items", - "position": "Right", - "title": "Select Item" - }, - { - "child_doctype": "Sales Order Item", - "description": "Enter quantity.", - "field": "", - "fieldname": "qty", - "fieldtype": "Float", - "has_next_condition": 0, - "is_table_field": 1, - "label": "Quantity", - "parent_field": "", - "parent_fieldname": "items", - "position": "Right", - "title": "Enter Quantity" - }, - { - "child_doctype": "Sales Order Item", - "description": "Enter rate of the item.", - "field": "", - "fieldname": "rate", - "fieldtype": "Currency", - "has_next_condition": 0, - "is_table_field": 1, - "label": "Rate", - "parent_field": "", - "parent_fieldname": "items", - "position": "Right", - "title": "Enter Rate" - }, - { - "description": "You can add sales taxes and charges here.", - "field": "", - "fieldname": "taxes", - "fieldtype": "Table", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Sales Taxes and Charges", - "parent_field": "", - "position": "Bottom", - "title": "Add Sales Taxes and Charges" - } - ], - "title": "Sales Order" -} \ No newline at end of file diff --git a/erpnext/selling/form_tour/selling_settings/selling_settings.json b/erpnext/selling/form_tour/selling_settings/selling_settings.json deleted file mode 100644 index 20c718f8c0..0000000000 --- a/erpnext/selling/form_tour/selling_settings/selling_settings.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "creation": "2021-06-29 20:39:19.408763", - "docstatus": 0, - "doctype": "Form Tour", - "idx": 0, - "is_standard": 1, - "modified": "2021-06-29 20:49:01.359489", - "modified_by": "Administrator", - "module": "Selling", - "name": "Selling Settings", - "owner": "Administrator", - "reference_doctype": "Selling Settings", - "save_on_complete": 0, - "steps": [ - { - "description": "By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a Naming Series. Choose the 'Naming Series' option.", - "field": "", - "fieldname": "cust_master_name", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Customer Naming By", - "parent_field": "", - "position": "Right", - "title": "Customer Naming By" - }, - { - "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice or Delivery Note without creating a Sales Order first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Sales Order' checkbox in the Customer master.", - "field": "", - "fieldname": "so_required", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", - "parent_field": "", - "position": "Left", - "title": "Sales Order Required for Sales Invoice & Delivery Note Creation" - }, - { - "description": "If this option is configured 'Yes', ERPNext will prevent you from creating a Sales Invoice without creating a Delivery Note first. This configuration can be overridden for a particular Customer by enabling the 'Allow Sales Invoice Creation Without Delivery Note' checkbox in the Customer master.", - "field": "", - "fieldname": "dn_required", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Is Delivery Note Required for Sales Invoice Creation?", - "parent_field": "", - "position": "Left", - "title": "Delivery Note Required for Sales Invoice Creation" - }, - { - "description": "Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.", - "field": "", - "fieldname": "selling_price_list", - "fieldtype": "Link", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Default Price List", - "parent_field": "", - "position": "Right", - "title": "Default Selling Price List" - } - ], - "title": "Selling Settings" -} \ No newline at end of file diff --git a/erpnext/selling/module_onboarding/selling/selling.json b/erpnext/selling/module_onboarding/selling/selling.json index 29f51cbbc6..160208ff68 100644 --- a/erpnext/selling/module_onboarding/selling/selling.json +++ b/erpnext/selling/module_onboarding/selling/selling.json @@ -19,7 +19,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/selling", "idx": 0, "is_complete": 0, - "modified": "2021-06-29 21:23:24.510122", + "modified": "2020-07-08 14:05:37.669753", "modified_by": "Administrator", "module": "Selling", "name": "Selling", @@ -41,7 +41,7 @@ "step": "Create a Quotation" }, { - "step": "Create a Sales Order" + "step": "Create your first Sales Order" }, { "step": "Selling Settings" diff --git a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json index 64defbfe3d..5a403b06cf 100644 --- a/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json +++ b/erpnext/selling/onboarding_step/create_a_customer/create_a_customer.json @@ -5,6 +5,7 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:16:19.731719", @@ -12,7 +13,6 @@ "name": "Create a Customer", "owner": "Administrator", "reference_document": "Customer", - "show_form_tour": 0, "show_full_form": 0, "title": "Create a Customer", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json index 52a5861551..d2068e167b 100644 --- a/erpnext/selling/onboarding_step/create_a_product/create_a_product.json +++ b/erpnext/selling/onboarding_step/create_a_product/create_a_product.json @@ -5,14 +5,14 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.133574", + "modified": "2020-05-12 18:30:02.489949", "modified_by": "Administrator", "name": "Create a Product", "owner": "Administrator", "reference_document": "Item", - "show_form_tour": 0, "show_full_form": 0, "title": "Create a Product", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json index 6f1837e24c..27253d15b6 100644 --- a/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json +++ b/erpnext/selling/onboarding_step/create_a_quotation/create_a_quotation.json @@ -5,6 +5,7 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:34:58.958641", @@ -12,7 +13,6 @@ "name": "Create a Quotation", "owner": "Administrator", "reference_document": "Quotation", - "show_form_tour": 0, "show_full_form": 1, "title": "Create a Quotation", "validate_action": 1 diff --git a/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json b/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json deleted file mode 100644 index 378f295f0c..0000000000 --- a/erpnext/selling/onboarding_step/create_a_sales_order/create_a_sales_order.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "action": "Create Entry", - "action_label": "Create Sales Order", - "creation": "2021-06-29 21:22:54.204880", - "description": "A Sales Order is a confirmation of an order from your customer. It is also referred to as Proforma Invoice.\n\nSales Order at the heart of your sales and purchase transactions. Sales Orders are linked in Delivery Note, Sales Invoices, Material Request, and Maintenance transactions. Through Sales Order, you can track fulfillment of the overall deal towards the customer.", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2021-06-29 21:22:54.204880", - "modified_by": "Administrator", - "name": "Create a Sales Order", - "owner": "Administrator", - "reference_document": "Sales Order", - "show_form_tour": 1, - "show_full_form": 1, - "title": "Create a Sales Order", - "validate_action": 1 -} \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json index 636453363b..d21c1f4954 100644 --- a/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json +++ b/erpnext/selling/onboarding_step/introduction_to_selling/introduction_to_selling.json @@ -5,13 +5,13 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, "modified": "2020-06-01 13:29:13.703177", "modified_by": "Administrator", "name": "Introduction to Selling", "owner": "Administrator", - "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Selling", "validate_action": 1, diff --git a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json index af95aa756f..7996d7b159 100644 --- a/erpnext/selling/onboarding_step/selling_settings/selling_settings.json +++ b/erpnext/selling/onboarding_step/selling_settings/selling_settings.json @@ -1,21 +1,19 @@ { "action": "Show Form Tour", - "action_label": "Let\u2019s walk-through Selling Settings", "creation": "2020-06-01 13:01:45.615189", - "description": "CRM and Selling module\u2019s features are configurable as per your business needs. Selling Settings is the place where you can set your preferences for:\n1. Customer naming and default values\n2. Billing and shipping preference in sales transactions\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2021-06-29 20:52:03.640254", + "modified": "2020-06-01 13:04:14.980743", "modified_by": "Administrator", "name": "Selling Settings", "owner": "Administrator", "reference_document": "Selling Settings", - "show_form_tour": 0, "show_full_form": 0, - "title": "Selling Settings", + "title": "Configure Selling Settings.", "validate_action": 0 } \ No newline at end of file diff --git a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 1e20eb0eba..9457deee26 100644 --- a/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/selling/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, + "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:25.538900", + "modified": "2020-07-04 12:33:16.970031", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", - "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 From 862ce916ae1e9b3ab60f4a2316c47a7f1aa6aa3c Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> Date: Fri, 9 Jul 2021 21:52:50 +0530 Subject: [PATCH 400/550] fix: Nested/Multi-level BOM help link (#26409) Updated the link for multi-level boms. Current link is broken. --- erpnext/public/js/help_links.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index aa9bba17c7..5c9a453e7d 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -991,7 +991,7 @@ frappe.help.help_links["Form/BOM"] = [ label: "Nested BOM Structure", url: docsUrl + - "user/manual/en/manufacturing/articles/nested-bom-structure", + "user/manual/en/manufacturing/articles/managing-multi-level-bom", }, ]; From 727e5d16bcf381fc2a35d41cb0e21e45a3856c24 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Jul 2021 21:55:37 +0530 Subject: [PATCH 401/550] chore: add .backportrc to gitignore (#26403) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 63c51c4976..89f56263ff 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ __pycache__ .idea/ .vscode/ node_modules/ +.backportrc.json \ No newline at end of file From c8a825c4783ebef33ed89f5cab5cdfe4d70fd326 Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 10 Jul 2021 18:24:24 +0530 Subject: [PATCH 402/550] chore: Test case for QI Rejection in Stock Entry - Use `get_single_value` instead of `get_doc` in validation - Test Case to check impact of stock settings on SE with rejected qi --- erpnext/controllers/stock_controller.py | 4 +- .../test_quality_inspection.py | 48 +++++++++++++++++-- .../doctype/stock_entry/stock_entry_utils.py | 2 + .../stock_settings/stock_settings.json | 2 +- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 1749297ce3..2526e6df0e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -395,7 +395,7 @@ class StockController(AccountsController): def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted or "Stop" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") if not qa_docstatus == 1: @@ -408,7 +408,7 @@ class StockController(AccountsController): def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_rejected or "Stop" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected") qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") if qa_status == "Rejected": diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 7f3d701034..f5d076a077 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -14,7 +14,7 @@ from erpnext.controllers.stock_controller import ( ) from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # test_records = frappe.get_test_records('Quality Inspection') @@ -159,6 +159,47 @@ class TestQualityInspection(unittest.TestCase): frappe.delete_doc("Quality Inspection", qi) dn.delete() + def test_rejected_qi_validation(self): + """Test if rejected QI blocks Stock Entry as per Stock Settings.""" + se = make_stock_entry( + item_code="_Test Item with QA", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=100, + inspection_required=True, + do_not_submit=True + ) + + readings = [ + { + "specification": "Iron Content", + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + } + ] + + qa = create_quality_inspection( + reference_type="Stock Entry", + reference_name=se.name, + readings=readings, + status="Rejected" + ) + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + se.reload() + self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") + se.reload() + se.submit() # when allowed in Stock settings, allow rejected QI + + # teardown + qa.reload() + qa.cancel() + se.reload() + se.cancel() + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") def create_quality_inspection(**args): args = frappe._dict(args) @@ -175,12 +216,11 @@ def create_quality_inspection(**args): if not args.readings: create_quality_inspection_parameter("Size") readings = {"specification": "Size", "min_value": 0, "max_value": 10} + if args.status == "Rejected": + readings["reading_1"] = "12" # status is auto set in child on save else: readings = args.readings - if args.status == "Rejected": - readings["reading_1"] = "12" # status is auto set in child on save - if isinstance(readings, list): for entry in readings: create_quality_inspection_parameter(entry["specification"]) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b12a8547fe..563fcb0397 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -45,6 +45,8 @@ def make_stock_entry(**args): s.posting_date = args.posting_date if args.posting_time: s.posting_time = args.posting_time + if args.inspection_required: + s.inspection_required = args.inspection_required # map names if args.from_warehouse: diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index d07e26b536..2a9dcfb67e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -290,7 +290,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-21 16:17:42.159829", + "modified": "2021-07-10 16:17:42.159829", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From 9ac9a4ef2120ceccf1e3893cc20b5c0261ae7928 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 16:18:35 +0530 Subject: [PATCH 403/550] feat: Optionally allow rejected quality inspection on submission --- erpnext/controllers/stock_controller.py | 84 ++++++++++++------- .../stock_entry_detail.json | 3 +- .../stock_settings/stock_settings.json | 21 ++++- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8196cff849..fed7c0a581 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -356,42 +356,68 @@ class StockController(AccountsController): }, update_modified) def validate_inspection(self): - '''Checks if quality inspection is set for Items that require inspection. - On submit, throw an exception''' - inspection_required_fieldname = None - if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - inspection_required_fieldname = "inspection_required_before_purchase" - elif self.doctype in ["Delivery Note", "Sales Invoice"]: - inspection_required_fieldname = "inspection_required_before_delivery" + """Checks if quality inspection is set/ is valid for Items that require inspection.""" + inspection_fieldname_map = { + "Purchase Receipt": "inspection_required_before_purchase", + "Purchase Invoice": "inspection_required_before_purchase", + "Sales Invoice": "inspection_required_before_delivery", + "Delivery Note": "inspection_required_before_delivery" + } + inspection_required_fieldname = inspection_fieldname_map.get(self.doctype) + # return if inspection is not required on document level if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or (self.doctype == "Stock Entry" and not self.inspection_required) or (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)): return - for d in self.get('items'): - qa_required = False - if (inspection_required_fieldname and not d.quality_inspection and - frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)): - qa_required = True - elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse: - qa_required = True - if self.docstatus == 1 and d.quality_inspection: - qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection) - if qa_doc.docstatus == 0: - link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) - frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) + for row in self.get('items'): + qi_required = False + if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)): + qi_required = True + elif self.doctype == "Stock Entry" and row.t_warehouse: + qi_required = True # inward stock needs inspection - if qa_doc.status != 'Accepted': - frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") - .format(d.idx, d.item_code), QualityInspectionRejectedError) - elif qa_required : - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted - if self.docstatus==1 and action == 'Stop': - frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)), - exc=QualityInspectionRequiredError) - else: - frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code))) + if qi_required: # validate row only if inspection is required on item level + self.validate_qi_presence(row) + if self.docstatus == 1: + self.validate_qi_submission(row) + self.validate_qi_rejection(row) + + def validate_qi_presence(self, row): + """Check if QI is present on row level. Warn on save and stop on submit if missing.""" + if not row.quality_inspection: + msg = _(f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}") + if self.docstatus == 1: + frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError) + else: + frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue") + + def validate_qi_submission(self, row): + """Check if QI is submitted on row level, during submission""" + action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted or "Stop" + qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") + + if not qa_docstatus == 1: + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = _(f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}") + if action == "Stop": + frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + else: + frappe.msgprint(msg, alert=True) + + def validate_qi_rejection(self, row): + """Check if QI is rejected on row level, during submission""" + action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_rejected or "Stop" + qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") + + if qa_status == "Rejected": + link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) + msg = _(f"Row #{row.idx}: Quality Inspection was rejected for item {row.item_code}") + if action == "Stop": + frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + else: + frappe.msgprint(msg, alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index a178283904..22f412a298 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -307,6 +307,7 @@ "fieldname": "quality_inspection", "fieldtype": "Link", "label": "Quality Inspection", + "no_copy": 1, "options": "Quality Inspection" }, { @@ -548,7 +549,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-22 20:08:23.799715", + "modified": "2021-06-21 16:03:18.834880", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index cf5d98d092..d07e26b536 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -23,7 +23,10 @@ "allow_negative_stock", "show_barcode_field", "clean_description_html", + "quality_inspection_settings_section", "action_if_quality_inspection_is_not_submitted", + "column_break_21", + "action_if_quality_inspection_is_rejected", "section_break_7", "automatically_set_serial_nos_based_on_fifo", "set_qty_in_transactions_based_on_serial_no_input", @@ -264,6 +267,22 @@ { "fieldname": "column_break_31", "fieldtype": "Column Break" + }, + { + "fieldname": "quality_inspection_settings_section", + "fieldtype": "Section Break", + "label": "Quality Inspection Settings" + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "default": "Stop", + "fieldname": "action_if_quality_inspection_is_rejected", + "fieldtype": "Select", + "label": "Action If Quality Inspection Is Rejected", + "options": "Stop\nWarn" } ], "icon": "icon-cog", @@ -271,7 +290,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-04-30 17:27:42.709231", + "modified": "2021-06-21 16:17:42.159829", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From 654e9d85d182bd486b74cd72eebf6f254391a35f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 21 Jun 2021 16:51:12 +0530 Subject: [PATCH 404/550] fix: sider and semgrep --- erpnext/controllers/stock_controller.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fed7c0a581..59d34d1ce5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -387,11 +387,11 @@ class StockController(AccountsController): def validate_qi_presence(self, row): """Check if QI is present on row level. Warn on save and stop on submit if missing.""" if not row.quality_inspection: - msg = _(f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}") + msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" if self.docstatus == 1: - frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError) + frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) else: - frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue") + frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" @@ -400,11 +400,11 @@ class StockController(AccountsController): if not qa_docstatus == 1: link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) - msg = _(f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}") + msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" if action == "Stop": - frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: - frappe.msgprint(msg, alert=True) + frappe.msgprint(_(msg), alert=True) def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" @@ -413,11 +413,11 @@ class StockController(AccountsController): if qa_status == "Rejected": link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection) - msg = _(f"Row #{row.idx}: Quality Inspection was rejected for item {row.item_code}") + msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" if action == "Stop": - frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) else: - frappe.msgprint(msg, alert=True, indicator="orange") + frappe.msgprint(_(msg), alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) From 9ba3fce4fd1cb300576d6ddffb79bd008a2ee003 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 22 Jun 2021 11:20:17 +0530 Subject: [PATCH 405/550] fix: Consistent alert indicators --- erpnext/controllers/stock_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 59d34d1ce5..1749297ce3 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -404,7 +404,7 @@ class StockController(AccountsController): if action == "Stop": frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: - frappe.msgprint(_(msg), alert=True) + frappe.msgprint(_(msg), alert=True, indicator="orange") def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" From f67f13c3844ccb96308f2ef9825a7d55713a2ae4 Mon Sep 17 00:00:00 2001 From: marination Date: Sat, 10 Jul 2021 18:24:24 +0530 Subject: [PATCH 406/550] chore: Test case for QI Rejection in Stock Entry - Use `get_single_value` instead of `get_doc` in validation - Test Case to check impact of stock settings on SE with rejected qi --- erpnext/controllers/stock_controller.py | 4 +- .../test_quality_inspection.py | 48 +++++++++++++++++-- .../doctype/stock_entry/stock_entry_utils.py | 2 + .../stock_settings/stock_settings.json | 2 +- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 1749297ce3..2526e6df0e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -395,7 +395,7 @@ class StockController(AccountsController): def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted or "Stop" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted") qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus") if not qa_docstatus == 1: @@ -408,7 +408,7 @@ class StockController(AccountsController): def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" - action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_rejected or "Stop" + action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected") qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status") if qa_status == "Rejected": diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 7f3d701034..f5d076a077 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -14,7 +14,7 @@ from erpnext.controllers.stock_controller import ( ) from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # test_records = frappe.get_test_records('Quality Inspection') @@ -159,6 +159,47 @@ class TestQualityInspection(unittest.TestCase): frappe.delete_doc("Quality Inspection", qi) dn.delete() + def test_rejected_qi_validation(self): + """Test if rejected QI blocks Stock Entry as per Stock Settings.""" + se = make_stock_entry( + item_code="_Test Item with QA", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=100, + inspection_required=True, + do_not_submit=True + ) + + readings = [ + { + "specification": "Iron Content", + "min_value": 0.1, + "max_value": 0.9, + "reading_1": "0.4" + } + ] + + qa = create_quality_inspection( + reference_type="Stock Entry", + reference_name=se.name, + readings=readings, + status="Rejected" + ) + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + se.reload() + self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") + se.reload() + se.submit() # when allowed in Stock settings, allow rejected QI + + # teardown + qa.reload() + qa.cancel() + se.reload() + se.cancel() + frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") def create_quality_inspection(**args): args = frappe._dict(args) @@ -175,12 +216,11 @@ def create_quality_inspection(**args): if not args.readings: create_quality_inspection_parameter("Size") readings = {"specification": "Size", "min_value": 0, "max_value": 10} + if args.status == "Rejected": + readings["reading_1"] = "12" # status is auto set in child on save else: readings = args.readings - if args.status == "Rejected": - readings["reading_1"] = "12" # status is auto set in child on save - if isinstance(readings, list): for entry in readings: create_quality_inspection_parameter(entry["specification"]) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index b12a8547fe..563fcb0397 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -45,6 +45,8 @@ def make_stock_entry(**args): s.posting_date = args.posting_date if args.posting_time: s.posting_time = args.posting_time + if args.inspection_required: + s.inspection_required = args.inspection_required # map names if args.from_warehouse: diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index d07e26b536..2a9dcfb67e 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -290,7 +290,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-06-21 16:17:42.159829", + "modified": "2021-07-10 16:17:42.159829", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", From 34a5cb9106db34fccb4eb570d7ea81f61926eb3b Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 2 Jul 2021 22:02:07 +0530 Subject: [PATCH 407/550] fix: Sider --- erpnext/buying/doctype/supplier/supplier.js | 4 ++-- erpnext/selling/doctype/customer/customer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index af6401b3fe..1766c2c80c 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -72,8 +72,8 @@ frappe.ui.form.on("Supplier", { frappe.call({ method: "get_supplier_group_details", doc: frm.doc, - callback: function(r){ - frm.refresh() + callback: function() { + frm.refresh(); } }); }, diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 91944adef3..2849466267 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -153,8 +153,8 @@ frappe.ui.form.on("Customer", { frappe.call({ method: "get_customer_group_details", doc: frm.doc, - callback: function(r){ - frm.refresh() + callback: function() { + frm.refresh(); } }); From 0be6583083cd3c06c6c828af4792bb531685457f Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 12 Jul 2021 09:18:19 +0530 Subject: [PATCH 408/550] refactor: suggested changes --- erpnext/buying/doctype/supplier/supplier.py | 6 ++-- erpnext/selling/doctype/customer/customer.py | 29 +++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 791f71ed3b..fd16b23c22 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -57,16 +57,16 @@ class Supplier(TransactionBase): self.payment_terms = "" self.accounts = [] - if not self.accounts and doc.accounts: + if doc.accounts: for account in doc.accounts: child = self.append('accounts') child.company = account.company child.account = account.account - self.save() - if not self.payment_terms and doc.payment_terms: + if doc.payment_terms: self.payment_terms = doc.payment_terms + self.save() def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index cdeb089618..3b62081e24 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -84,25 +84,22 @@ class Customer(TransactionBase): self.accounts = self.credit_limits = [] self.payment_terms = self.default_price_list = "" - if not self.accounts and doc.accounts: - for account in doc.accounts: - child = self.append('accounts') - child.company = account.company - child.account = account.account - self.save() + tables = [["accounts", "account"], ["credit_limits", "credit_limit"]] + fields = ["payment_terms", "default_price_list"] - if not self.credit_limits and doc.credit_limits: - for credit in doc.credit_limits: - child = self.append('credit_limits') - child.company = credit.company - child.credit_limit = credit.credit_limit - self.save() + for row in tables: + table, field = row[0], row[1] + if not doc.get(table): continue - if not self.payment_terms and doc.payment_terms: - self.payment_terms = doc.payment_terms + for entry in doc.get(table): + child = self.append(table) + child.update({"company": entry.company, field: entry.get(field)}) - if not self.default_price_list and doc.default_price_list: - self.default_price_list = doc.default_price_list + for field in fields: + if not doc.get(field): continue + self.update({field: doc.get(field)}) + + self.save() def check_customer_group_change(self): frappe.flags.customer_group_changed = False From b99469cdabc646cdfa55ae1ce1709374c7246a7a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 20:18:53 +0530 Subject: [PATCH 409/550] fix: stock levels disapperaing on refresh refresh_section removes all sections with `custom` class, added different class to avoid this behaviour. --- erpnext/stock/doctype/item/item.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 45e3c21b27..568f0ef451 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -93,7 +93,7 @@ frappe.ui.form.on("Item", { erpnext.item.edit_prices_button(frm); erpnext.item.toggle_attributes(frm); - + if (!frm.doc.is_fixed_asset) { erpnext.item.make_dashboard(frm); } @@ -381,7 +381,8 @@ $.extend(erpnext.item, { // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { frappe.require('item-dashboard.bundle.js', function() { - const section = frm.dashboard.add_section('', __("Stock Levels")); + frm.dashboard.parent.find('.stock-levels').remove(); + const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels'); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, From bedb0addf0ca7e81ca10379f1830d42167811ca4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 20:36:51 +0530 Subject: [PATCH 410/550] test: ui test for stock levels --- cypress/integration/test_item.js | 44 ++++++++++++++++++++++++++++++++ cypress/support/commands.js | 6 +++++ 2 files changed, 50 insertions(+) create mode 100644 cypress/integration/test_item.js diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js new file mode 100644 index 0000000000..fcb7533a22 --- /dev/null +++ b/cypress/integration/test_item.js @@ -0,0 +1,44 @@ +describe("Test Item Dashboard", () => { + before(() => { + cy.login(); + cy.visit("/app/item"); + cy.insert_doc( + "Item", + { + item_code: "e2e_test_item", + item_group: "All Item Groups", + opening_stock: 42, + valuation_rate: 100, + }, + true + ); + cy.go_to_doc("item", "e2e_test_item"); + }); + + it("should show dashboard with correct data on first load", () => { + cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); + cy.get(".stock-levels").contains("e2e_test_item").should("exist"); + + // reserved and available qty + cy.get(".stock-levels .inline-graph-count") + .eq(0) + .contains("0") + .should("exist"); + cy.get(".stock-levels .inline-graph-count") + .eq(1) + .contains("42") + .should("exist"); + }); + + it("should persist on field change", () => { + cy.get('input[data-fieldname="disabled"]').check(); + cy.wait(500); + cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); + cy.get(".stock-levels").should("have.length", 1); + }); + + it("should persist on reload", () => { + cy.reload(); + cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7929a2e0ef..7ddc80ab8d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -23,3 +23,9 @@ // // -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); + +const slug = (name) => name.toLowerCase().replace(" ", "-"); + +Cypress.Commands.add("go_to_doc", (doctype, name) => { + cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`); +}); From 203c2b22924d9a8c25ba2bec7c3417e6733b3992 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 22:35:15 +0530 Subject: [PATCH 411/550] ci: show bench logs if ui test fails --- .github/helper/install.sh | 2 +- .github/workflows/ui-tests.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index f7a7122343..455ab861f9 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -42,6 +42,6 @@ sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile bench get-app erpnext "${GITHUB_WORKSPACE}" -bench start & +bench start &> bench_run_logs.txt & bench --site test_site reinstall --yes bench build --app frappe diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 4bc55da1d8..412a05b0a1 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -102,3 +102,7 @@ jobs: run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + - name: Show bench console if tests failed + if: ${{ failure() }} + run: cat ~/frappe-bench/bench_run_logs.txt From 22cb6428316b8498103e68694902aed051a88359 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 2 Jul 2021 23:32:33 +0530 Subject: [PATCH 412/550] chore: update test company FY --- erpnext/setup/utils.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 13269a8282..d5dbd4cc65 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -28,21 +28,21 @@ def before_tests(): from frappe.desk.page.setup_wizard.setup_wizard import setup_complete if not frappe.get_list("Company"): setup_complete({ - "currency" :"USD", - "full_name" :"Test User", - "company_name" :"Wind Power LLC", - "timezone" :"America/New_York", - "company_abbr" :"WP", - "industry" :"Manufacturing", - "country" :"United States", - "fy_start_date" :"2011-01-01", - "fy_end_date" :"2011-12-31", - "language" :"english", - "company_tagline" :"Testing", - "email" :"test@erpnext.com", - "password" :"test", + "currency" :"USD", + "full_name" :"Test User", + "company_name" :"Wind Power LLC", + "timezone" :"America/New_York", + "company_abbr" :"WP", + "industry" :"Manufacturing", + "country" :"United States", + "fy_start_date" :"2021-01-01", + "fy_end_date" :"2021-12-31", + "language" :"english", + "company_tagline" :"Testing", + "email" :"test@erpnext.com", + "password" :"test", "chart_of_accounts" : "Standard", - "domains" : ["Manufacturing"], + "domains" : ["Manufacturing"], }) frappe.db.sql("delete from `tabLeave Allocation`") From caacd0ad2c68f8dc0f14b96d904ac552c745b74a Mon Sep 17 00:00:00 2001 From: Ankush Date: Mon, 12 Jul 2021 10:20:19 +0530 Subject: [PATCH 413/550] fix: stock levels disapperaing on refresh (bp #26305) refresh_section removes all sections with `custom` class, added different class to avoid this behaviour. # Conflicts: # erpnext/stock/doctype/item/item.js --- erpnext/stock/doctype/item/item.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 8aec89381a..b55374b8d8 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -93,7 +93,7 @@ frappe.ui.form.on("Item", { erpnext.item.edit_prices_button(frm); erpnext.item.toggle_attributes(frm); - + if (!frm.doc.is_fixed_asset) { erpnext.item.make_dashboard(frm); } @@ -381,7 +381,8 @@ $.extend(erpnext.item, { // Show Stock Levels only if is_stock_item if (frm.doc.is_stock_item) { frappe.require('assets/js/item-dashboard.min.js', function() { - const section = frm.dashboard.add_section('', __("Stock Levels")); + frm.dashboard.parent.find('.stock-levels').remove(); + const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels'); erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({ parent: section, item_code: frm.doc.name, From 432d8efa3d61dc489be45553ce8f2ab1da8efbb7 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Jul 2021 10:47:40 +0530 Subject: [PATCH 414/550] fix(pos): taxes amount in pos item cart (#26411) --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 7cae0e4797..38508c219b 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -472,12 +472,7 @@ erpnext.PointOfSale.ItemCart = class { const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; this.render_grand_total(grand_total); - const taxes = frm.doc.taxes.map(t => { - return { - description: t.description, rate: t.rate - }; - }); - this.render_taxes(frm.doc.total_taxes_and_charges, taxes); + this.render_taxes(frm.doc.taxes); } render_net_total(value) { @@ -502,14 +497,14 @@ erpnext.PointOfSale.ItemCart = class { ); } - render_taxes(value, taxes) { + render_taxes(taxes) { if (taxes.length) { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`; return `
    ${description}
    -
    ${format_currency(value, currency)}
    +
    ${format_currency(t.tax_amount_after_discount_amount, currency)}
    `; }).join(''); this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); From 6467632f6088f64baadf9235ebeede7385bcc0a9 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:07:06 +0530 Subject: [PATCH 415/550] fix: error popup for COA errors (#26357) --- .../chart_of_accounts_importer.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 3b764aab10..cb1f2df7f0 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -13,7 +13,9 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file class ChartofAccountsImporter(Document): - pass + def validate(self): + validate_accounts(self.import_file) + @frappe.whitelist() def validate_company(company): @@ -301,28 +303,28 @@ def validate_accounts(file_name): if account["parent_account"] and accounts_dict.get(account["parent_account"]): accounts_dict[account["parent_account"]]["is_group"] = 1 - message = validate_root(accounts_dict) - if message: return message - message = validate_account_types(accounts_dict) - if message: return message + validate_root(accounts_dict) + + validate_account_types(accounts_dict) + return [True, len(accounts)] def validate_root(accounts): roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] if len(roots) < 4: - return _("Number of root accounts cannot be less than 4") + frappe.throw(_("Number of root accounts cannot be less than 4")) error_messages = [] for account in roots: if not account.get("root_type") and account.get("account_name"): - error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name"))) + error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name"))) elif account.get("root_type") not in get_root_types() and account.get("account_name"): - error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name"))) + error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name"))) if error_messages: - return "
    ".join(error_messages) + frappe.throw("
    ".join(error_messages)) def get_root_types(): return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') @@ -356,7 +358,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_ledger) - set(account_types)) if missing: - return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))) account_types_for_group = ["Bank", "Cash", "Stock"] # fix logic bug @@ -364,7 +366,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_group) - set(account_groups)) if missing: - return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))) def unset_existing_data(company): linked = frappe.db.sql('''select fieldname from tabDocField From f60c3f06554206aec236690ae0ea975ddba981ff Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:07:30 +0530 Subject: [PATCH 416/550] fix: error popup for COA errors (#26358) --- .../chart_of_accounts_importer.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 3b764aab10..4fd8413d83 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -13,7 +13,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file, read_xls_file_from_attached_file class ChartofAccountsImporter(Document): - pass + def validate(self): + validate_accounts(self.import_file) @frappe.whitelist() def validate_company(company): @@ -301,28 +302,27 @@ def validate_accounts(file_name): if account["parent_account"] and accounts_dict.get(account["parent_account"]): accounts_dict[account["parent_account"]]["is_group"] = 1 - message = validate_root(accounts_dict) - if message: return message - message = validate_account_types(accounts_dict) - if message: return message + validate_root(accounts_dict) + + validate_account_types(accounts_dict) return [True, len(accounts)] def validate_root(accounts): roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] if len(roots) < 4: - return _("Number of root accounts cannot be less than 4") + frappe.throw(_("Number of root accounts cannot be less than 4")) error_messages = [] for account in roots: if not account.get("root_type") and account.get("account_name"): - error_messages.append("Please enter Root Type for account- {0}".format(account.get("account_name"))) + error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name"))) elif account.get("root_type") not in get_root_types() and account.get("account_name"): - error_messages.append("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity".format(account.get("account_name"))) + error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name"))) if error_messages: - return "
    ".join(error_messages) + frappe.throw("
    ".join(error_messages)) def get_root_types(): return ('Asset', 'Liability', 'Expense', 'Income', 'Equity') @@ -356,7 +356,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_ledger) - set(account_types)) if missing: - return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))) account_types_for_group = ["Bank", "Cash", "Stock"] # fix logic bug @@ -364,7 +364,7 @@ def validate_account_types(accounts): missing = list(set(account_types_for_group) - set(account_groups)) if missing: - return _("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)) + frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing))) def unset_existing_data(company): linked = frappe.db.sql('''select fieldname from tabDocField From 4b4ac7c05198f778be01c197e04482ab8e4cf142 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:10:00 +0530 Subject: [PATCH 417/550] fix(report): iterate on accounts only when accounts exist (#26390) --- erpnext/accounts/report/general_ledger/general_ledger.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 744ada9e55..e724e9b51b 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -48,13 +48,12 @@ def validate_filters(filters, account_details): if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - - for account in filters.account: - if not account_details.get(account): - frappe.throw(_("Account {0} does not exists").format(account)) if filters.get('account'): filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if not account_details.get(account): + frappe.throw(_("Account {0} does not exists").format(account)) if (filters.get("account") and filters.get("group_by") == _('Group by Account') and account_details[filters.account].is_group == 0): From bf03671a334e65c3151d11a9c92e6d73f6edac97 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:10:28 +0530 Subject: [PATCH 418/550] fix(report): iterate on accounts only when accounts exist (#26391) --- erpnext/accounts/report/general_ledger/general_ledger.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 744ada9e55..e724e9b51b 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -48,13 +48,12 @@ def validate_filters(filters, account_details): if not filters.get("from_date") and not filters.get("to_date"): frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) - - for account in filters.account: - if not account_details.get(account): - frappe.throw(_("Account {0} does not exists").format(account)) if filters.get('account'): filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if not account_details.get(account): + frappe.throw(_("Account {0} does not exists").format(account)) if (filters.get("account") and filters.get("group_by") == _('Group by Account') and account_details[filters.account].is_group == 0): From 10473b1195f8bb2d68c524a95bc66e87eea07862 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:11:29 +0530 Subject: [PATCH 419/550] fix: dunning calculation of grand total when rate of interest is 0% (#26285) --- erpnext/accounts/doctype/dunning/dunning.py | 8 +-- .../accounts/doctype/dunning/test_dunning.py | 49 ++++++++++++++++++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c6c689212b..1ef512a489 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -25,7 +25,7 @@ class Dunning(AccountsController): def validate_amount(self): amounts = calculate_interest_and_amount( - self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) + self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) if self.interest_amount != amounts.get('interest_amount'): self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) if self.dunning_amount != amounts.get('dunning_amount'): @@ -91,13 +91,13 @@ def resolve_dunning(doc, state): for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') -def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): +def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): interest_amount = 0 - grand_total = 0 + grand_total = flt(outstanding_amount) + flt(dunning_fee) if rate_of_interest: interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) + grand_total += flt(interest_amount) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { 'interest_amount': interest_amount, diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e2d4d82e41..ed50f784b2 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase): @classmethod def setUpClass(self): create_dunning_type() + create_dunning_type_with_zero_interest_rate() unlink_payment_on_cancel_of_invoice() @classmethod @@ -25,11 +26,20 @@ class TestDunning(unittest.TestCase): def test_dunning(self): dunning = create_dunning() amounts = calculate_interest_and_amount( - dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) + def test_dunning_with_zero_interest_rate(self): + dunning = create_dunning_with_zero_interest_rate() + amounts = calculate_interest_and_amount( + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + self.assertEqual(round(amounts.get('interest_amount'), 2), 0) + self.assertEqual(round(amounts.get('dunning_amount'), 2), 20) + self.assertEqual(round(amounts.get('grand_total'), 2), 120) + + def test_gl_entries(self): dunning = create_dunning() dunning.submit() @@ -83,6 +93,27 @@ def create_dunning(): dunning.save() return dunning +def create_dunning_with_zero_interest_rate(): + posting_date = add_days(today(), -20) + due_date = add_days(today(), -15) + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=posting_date, due_date=due_date, status='Overdue') + dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest') + dunning = frappe.new_doc("Dunning") + dunning.sales_invoice = sales_invoice.name + dunning.customer_name = sales_invoice.customer_name + dunning.outstanding_amount = sales_invoice.outstanding_amount + dunning.debit_to = sales_invoice.debit_to + dunning.currency = sales_invoice.currency + dunning.company = sales_invoice.company + dunning.posting_date = nowdate() + dunning.due_date = sales_invoice.due_date + dunning.dunning_type = 'First Notice with 0% Rate of Interest' + dunning.rate_of_interest = dunning_type.rate_of_interest + dunning.dunning_fee = dunning_type.dunning_fee + dunning.save() + return dunning + def create_dunning_type(): dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = 'First Notice' @@ -98,3 +129,19 @@ def create_dunning_type(): } ) dunning_type.save() + +def create_dunning_type_with_zero_interest_rate(): + dunning_type = frappe.new_doc("Dunning Type") + dunning_type.dunning_type = 'First Notice with 0% Rate of Interest' + dunning_type.start_day = 10 + dunning_type.end_day = 20 + dunning_type.dunning_fee = 20 + dunning_type.rate_of_interest = 0 + dunning_type.append( + "dunning_letter_text", { + 'language': 'en', + 'body_text': 'We have still not received payment for our invoice ', + 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.' + } + ) + dunning_type.save() \ No newline at end of file From 7e05bea6a9fd3d2d6402c5e22d11b68e1225a774 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Mon, 12 Jul 2021 11:11:58 +0530 Subject: [PATCH 420/550] fix: dunning calculation of grand total when rate of interest is 0% (#26286) --- erpnext/accounts/doctype/dunning/dunning.py | 8 ++-- .../accounts/doctype/dunning/test_dunning.py | 48 ++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index c6c689212b..1ef512a489 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -25,7 +25,7 @@ class Dunning(AccountsController): def validate_amount(self): amounts = calculate_interest_and_amount( - self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) + self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) if self.interest_amount != amounts.get('interest_amount'): self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) if self.dunning_amount != amounts.get('dunning_amount'): @@ -91,13 +91,13 @@ def resolve_dunning(doc, state): for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') -def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): +def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): interest_amount = 0 - grand_total = 0 + grand_total = flt(outstanding_amount) + flt(dunning_fee) if rate_of_interest: interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) + grand_total += flt(interest_amount) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { 'interest_amount': interest_amount, diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index e2d4d82e41..31cb078cd4 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -16,6 +16,7 @@ class TestDunning(unittest.TestCase): @classmethod def setUpClass(self): create_dunning_type() + create_dunning_type_with_zero_interest_rate() unlink_payment_on_cancel_of_invoice() @classmethod @@ -25,11 +26,19 @@ class TestDunning(unittest.TestCase): def test_dunning(self): dunning = create_dunning() amounts = calculate_interest_and_amount( - dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) + def test_dunning_with_zero_interest_rate(self): + dunning = create_dunning_with_zero_interest_rate() + amounts = calculate_interest_and_amount( + dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) + self.assertEqual(round(amounts.get('interest_amount'), 2), 0) + self.assertEqual(round(amounts.get('dunning_amount'), 2), 20) + self.assertEqual(round(amounts.get('grand_total'), 2), 120) + def test_gl_entries(self): dunning = create_dunning() dunning.submit() @@ -83,6 +92,27 @@ def create_dunning(): dunning.save() return dunning +def create_dunning_with_zero_interest_rate(): + posting_date = add_days(today(), -20) + due_date = add_days(today(), -15) + sales_invoice = create_sales_invoice_against_cost_center( + posting_date=posting_date, due_date=due_date, status='Overdue') + dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest') + dunning = frappe.new_doc("Dunning") + dunning.sales_invoice = sales_invoice.name + dunning.customer_name = sales_invoice.customer_name + dunning.outstanding_amount = sales_invoice.outstanding_amount + dunning.debit_to = sales_invoice.debit_to + dunning.currency = sales_invoice.currency + dunning.company = sales_invoice.company + dunning.posting_date = nowdate() + dunning.due_date = sales_invoice.due_date + dunning.dunning_type = 'First Notice with 0% Rate of Interest' + dunning.rate_of_interest = dunning_type.rate_of_interest + dunning.dunning_fee = dunning_type.dunning_fee + dunning.save() + return dunning + def create_dunning_type(): dunning_type = frappe.new_doc("Dunning Type") dunning_type.dunning_type = 'First Notice' @@ -98,3 +128,19 @@ def create_dunning_type(): } ) dunning_type.save() + +def create_dunning_type_with_zero_interest_rate(): + dunning_type = frappe.new_doc("Dunning Type") + dunning_type.dunning_type = 'First Notice with 0% Rate of Interest' + dunning_type.start_day = 10 + dunning_type.end_day = 20 + dunning_type.dunning_fee = 20 + dunning_type.rate_of_interest = 0 + dunning_type.append( + "dunning_letter_text", { + 'language': 'en', + 'body_text': 'We have still not received payment for our invoice ', + 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.' + } + ) + dunning_type.save() \ No newline at end of file From 4ff98cc17186f964c92d8879318478994bc52950 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 21:17:17 +0530 Subject: [PATCH 421/550] fix: Incorrect discount amount on amended document --- erpnext/public/js/controllers/taxes_and_totals.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 0471704c01..181e340427 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -67,6 +67,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_discount_amount(){ if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { + this.calculate_item_values(); + this.calculate_net_total(); this.set_discount_amount(); this.apply_discount_amount(); } From ae41b53ceed0f07fd03960151b5fd48b2aa66a2f Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 6 Jul 2021 18:00:35 +0530 Subject: [PATCH 422/550] fix: stock_rbnb not defined (#26354) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e488b695b5..82c87a83a5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -386,6 +386,7 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked + stock_rbnb = self.get_company_default("stock_received_but_not_billed") i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): From b4ea185ecaabe3c8080aa1bb0fdcb643d850f5d7 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Mon, 12 Jul 2021 13:15:41 +0530 Subject: [PATCH 423/550] fix: added company filter while fetching loans (#26295) * fix: added company filter while fetching loans * fix: added set_query in refresh * fix: quotes * fix: tests Co-authored-by: Rucha Mahabal --- erpnext/loan_management/doctype/loan/loan.js | 3 +- .../loan_application/loan_application.js | 13 +++++-- .../doctype/salary_slip/salary_slip.py | 1 + .../doctype/salary_slip/test_salary_slip.py | 14 ++++--- .../salary_structure/test_salary_structure.py | 37 +++++++++---------- 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 28af3a9c41..f9c201ab60 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -28,7 +28,8 @@ frappe.ui.form.on('Loan', { frm.set_query("loan_type", function () { return { "filters": { - "docstatus": 1 + "docstatus": 1, + "company": frm.doc.company } }; }); diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index 1365274971..017026ca13 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -14,11 +14,18 @@ frappe.ui.form.on('Loan Application', { refresh: function(frm) { frm.trigger("toggle_fields"); frm.trigger("add_toolbar_buttons"); + frm.set_query("loan_type", () => { + return { + filters: { + company: frm.doc.company + } + }; + }); }, repayment_method: function(frm) { - frm.doc.repayment_amount = frm.doc.repayment_periods = "" - frm.trigger("toggle_fields") - frm.trigger("toggle_required") + frm.doc.repayment_amount = frm.doc.repayment_periods = ""; + frm.trigger("toggle_fields"); + frm.trigger("toggle_required"); }, toggle_fields: function(frm) { frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period") diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index c55bec89be..f82b0d51bb 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1088,6 +1088,7 @@ class SalarySlip(TransactionBase): "applicant": self.employee, "docstatus": 1, "repay_from_salary": 1, + "company": self.company }) def make_loan_repayment_entry(self): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index ce88cc3f1e..d730fcf1fa 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -481,15 +481,19 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", + { + "user_id": user + }, + ["name", "company", "employee_name"], + as_dict=True) - employee = frappe.db.get_value("Employee", {"user_id": user}) - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) + salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) if not salary_slip_name: - salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee) - salary_slip.employee_name = frappe.get_value("Employee", - {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name") + salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name) + salary_slip.employee_name = employee.employee_name salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e7d123c996..374dd7ee44 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -119,26 +119,25 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) - if not frappe.db.exists('Salary Structure', salary_structure): - details = { - "doctype": "Salary Structure", - "name": salary_structure, - "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account", filters={'account_currency': currency}), - "currency": currency - } - if other_details and isinstance(other_details, dict): - details.update(other_details) - salary_structure_doc = frappe.get_doc(details) - salary_structure_doc.insert() - if not dont_submit: - salary_structure_doc.submit() + if frappe.db.exists("Salary Structure", salary_structure): + frappe.db.delete("Salary Structure", salary_structure) - else: - salary_structure_doc = frappe.get_doc("Salary Structure", salary_structure) + details = { + "doctype": "Salary Structure", + "name": salary_structure, + "company": company or erpnext.get_default_company(), + "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "payroll_frequency": payroll_frequency, + "payment_account": get_random("Account", filters={"account_currency": currency}), + "currency": currency + } + if other_details and isinstance(other_details, dict): + details.update(other_details) + salary_structure_doc = frappe.get_doc(details) + salary_structure_doc.insert() + if not dont_submit: + salary_structure_doc.submit() filters = {'employee':employee, 'docstatus': 1} if not from_date and payroll_period: From 01aada6c904e37ce5ef89980ae97b68fd4fbe257 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Jul 2021 13:24:43 +0530 Subject: [PATCH 424/550] refactor: Optimized code for reposting item valuation --- .../stock/doctype/stock_entry/stock_entry.py | 2 +- erpnext/stock/stock_ledger.py | 61 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8f27ef4356..90b81ddb1d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -529,7 +529,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4e9c7689ae..c15d1eda7d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -6,13 +6,14 @@ import frappe import erpnext import copy from frappe import _ -from frappe.utils import cint, flt, cstr, now, get_link_to_form +from frappe.utils import cint, flt, cstr, now, get_link_to_form, getdate from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel from erpnext.stock.utils import get_bin import json from six import iteritems + # future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): @@ -130,7 +131,13 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat if not args and voucher_type and voucher_no: args = get_args_for_voucher(voucher_type, voucher_no) - distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] + distinct_item_warehouses = {} + for i, d in enumerate(args): + distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ + "reposting_status": False, + "sle": d, + "args_idx": i + })) i = 0 while i < len(args): @@ -139,13 +146,21 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat "warehouse": args[i].warehouse, "posting_date": args[i].posting_date, "posting_time": args[i].posting_time, - "creation": args[i].get("creation") + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - for item_wh, new_sle in iteritems(obj.new_items): - if item_wh not in distinct_item_warehouses: - args.append(new_sle) + distinct_item_warehouses[(args[i].item_code, args[i].warehouse)].reposting_status = True + if obj.new_items_found: + for item_wh, data in iteritems(distinct_item_warehouses): + if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + data.args_idx = len(args) + args.append(data.sle) + elif data.sle_changed and not data.reposting_status: + args[data.args_idx] = data.sle + + data.sle_changed = False i += 1 def get_args_for_voucher(voucher_type, voucher_no): @@ -186,11 +201,12 @@ class update_entries_after(object): self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() self.valuation_method = get_valuation_method(self.item_code) - self.new_items = {} + + self.new_items_found = False + self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.data = frappe._dict() self.initialize_previous_data(self.args) - self.build() def get_precision(self): @@ -296,11 +312,29 @@ class update_entries_after(object): elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: return entries_to_fix elif dependant_sle.item_code != self.item_code: - if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: - self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle + self.update_distinct_item_warehouses(dependant_sle) return entries_to_fix elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix + else: + return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + + def update_distinct_item_warehouses(self, dependant_sle): + key = (dependant_sle.item_code, dependant_sle.warehouse) + val = frappe._dict({ + "sle": dependant_sle + }) + if key not in self.distinct_item_warehouses: + self.distinct_item_warehouses[key] = val + self.new_items_found = True + else: + existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): + val.sle_changed = True + self.distinct_item_warehouses[key] = val + self.new_items_found = True + + def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) args = self.data[dependant_sle.warehouse].previous_sle \ @@ -393,6 +427,7 @@ class update_entries_after(object): rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": + self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): @@ -442,7 +477,11 @@ class update_entries_after(object): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True) + if not sle.dependant_sle_voucher_detail_no: + self.recalculate_amounts_in_stock_entry(sle.voucher_no) + + def recalculate_amounts_in_stock_entry(self, voucher_no): + stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: From 0003938f2bba56ab1fad31d3b16ad87178194f19 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Jul 2021 13:24:43 +0530 Subject: [PATCH 425/550] refactor: Optimized code for reposting item valuation --- erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 0febcb6891..cb939e63c2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -178,3 +178,4 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) + frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"]) From 97bce3af9a3f5d2fd56c4e7f121708b486520cf6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Jul 2021 13:24:43 +0530 Subject: [PATCH 426/550] refactor: Optimized code for reposting item valuation --- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../stock_ledger_entry/stock_ledger_entry.py | 1 + erpnext/stock/stock_ledger.py | 61 +++++++++++++++---- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8f27ef4356..90b81ddb1d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -529,7 +529,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 0febcb6891..cb939e63c2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -178,3 +178,4 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) + frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"]) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4e9c7689ae..c15d1eda7d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -6,13 +6,14 @@ import frappe import erpnext import copy from frappe import _ -from frappe.utils import cint, flt, cstr, now, get_link_to_form +from frappe.utils import cint, flt, cstr, now, get_link_to_form, getdate from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel from erpnext.stock.utils import get_bin import json from six import iteritems + # future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): @@ -130,7 +131,13 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat if not args and voucher_type and voucher_no: args = get_args_for_voucher(voucher_type, voucher_no) - distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] + distinct_item_warehouses = {} + for i, d in enumerate(args): + distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ + "reposting_status": False, + "sle": d, + "args_idx": i + })) i = 0 while i < len(args): @@ -139,13 +146,21 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat "warehouse": args[i].warehouse, "posting_date": args[i].posting_date, "posting_time": args[i].posting_time, - "creation": args[i].get("creation") + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - for item_wh, new_sle in iteritems(obj.new_items): - if item_wh not in distinct_item_warehouses: - args.append(new_sle) + distinct_item_warehouses[(args[i].item_code, args[i].warehouse)].reposting_status = True + if obj.new_items_found: + for item_wh, data in iteritems(distinct_item_warehouses): + if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + data.args_idx = len(args) + args.append(data.sle) + elif data.sle_changed and not data.reposting_status: + args[data.args_idx] = data.sle + + data.sle_changed = False i += 1 def get_args_for_voucher(voucher_type, voucher_no): @@ -186,11 +201,12 @@ class update_entries_after(object): self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() self.valuation_method = get_valuation_method(self.item_code) - self.new_items = {} + + self.new_items_found = False + self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.data = frappe._dict() self.initialize_previous_data(self.args) - self.build() def get_precision(self): @@ -296,11 +312,29 @@ class update_entries_after(object): elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: return entries_to_fix elif dependant_sle.item_code != self.item_code: - if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: - self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle + self.update_distinct_item_warehouses(dependant_sle) return entries_to_fix elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix + else: + return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + + def update_distinct_item_warehouses(self, dependant_sle): + key = (dependant_sle.item_code, dependant_sle.warehouse) + val = frappe._dict({ + "sle": dependant_sle + }) + if key not in self.distinct_item_warehouses: + self.distinct_item_warehouses[key] = val + self.new_items_found = True + else: + existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): + val.sle_changed = True + self.distinct_item_warehouses[key] = val + self.new_items_found = True + + def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) args = self.data[dependant_sle.warehouse].previous_sle \ @@ -393,6 +427,7 @@ class update_entries_after(object): rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": + self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): @@ -442,7 +477,11 @@ class update_entries_after(object): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True) + if not sle.dependant_sle_voucher_detail_no: + self.recalculate_amounts_in_stock_entry(sle.voucher_no) + + def recalculate_amounts_in_stock_entry(self, voucher_no): + stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: From 9e1819d36675bec1096fb34f603889dc647ae290 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Jul 2021 14:31:57 +0530 Subject: [PATCH 427/550] fix: move the rename abbreviation job to long queue (#26434) --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 0427abe558..8fd905d7a7 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -395,7 +395,7 @@ class Company(NestedSet): @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue="long", company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) From a20999cfbcbb950bb1dc34838dc6bb1dd04d24ae Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Jul 2021 14:33:23 +0530 Subject: [PATCH 428/550] fix: exchange gain loss not set for advances linked with invoices (#25369) --- .../doctype/payment_entry/payment_entry.py | 18 +- .../payment_entry_reference.json | 12 +- .../purchase_invoice/purchase_invoice.py | 1 + .../purchase_invoice/test_purchase_invoice.py | 103 ++++++ .../purchase_invoice_advance.json | 330 ++++++----------- .../doctype/sales_invoice/sales_invoice.py | 1 + .../sales_invoice_advance.json | 331 ++++++------------ erpnext/accounts/utils.py | 14 +- erpnext/controllers/accounts_controller.py | 86 ++++- 9 files changed, 441 insertions(+), 455 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0c21aae944..ff00fde523 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -183,6 +183,13 @@ class PaymentEntry(AccountsController): d.reference_name, self.party_account_currency) for field, value in iteritems(ref_details): + if d.exchange_gain_loss: + # for cases where gain/loss is booked into invoice + # exchange_gain_loss is calculated from invoice & populated + # and row.exchange_rate is already set to payment entry's exchange rate + # refer -> `update_reference_in_payment_entry()` in utils.py + continue + if field == 'exchange_rate' or not d.get(field) or force: d.db_set(field, value) @@ -664,8 +671,8 @@ class PaymentEntry(AccountsController): gl_entries.append(gle) if self.unallocated_amount: - base_unallocated_amount = self.unallocated_amount * \ - (self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate) + exchange_rate = self.get_exchange_rate() + base_unallocated_amount = (self.unallocated_amount * exchange_rate) gle = party_gl_dict.copy() @@ -806,10 +813,17 @@ class PaymentEntry(AccountsController): if account_details: row.update(account_details) + + if not row.get('amount'): + # if no difference amount + return self.append('deductions', row) self.set_unallocated_amount() + def get_exchange_rate(self): + return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate + def initialize_taxes(self): for tax in self.get("taxes"): validate_taxes_and_charges(tax) diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 912ad0977a..43eb0b6e2a 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -14,7 +14,8 @@ "total_amount", "outstanding_amount", "allocated_amount", - "exchange_rate" + "exchange_rate", + "exchange_gain_loss" ], "fields": [ { @@ -90,12 +91,19 @@ "fieldtype": "Link", "label": "Payment Term", "options": "Payment Term" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-02-10 11:25:47.144392", + "modified": "2021-04-21 13:30:11.605388", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c1cc092554..b99d75ec49 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -451,6 +451,7 @@ class PurchaseInvoice(BuyingController): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 189260a29d..a04d082f19 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -974,6 +974,109 @@ class TestPurchaseInvoice(unittest.TestCase): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() + def test_gain_loss_with_advance_entry(self): + unlink_enabled = frappe.db.get_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice") + frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) + pay = frappe.get_doc({ + 'doctype': 'Payment Entry', + 'company': '_Test Company', + 'payment_type': 'Pay', + 'party_type': 'Supplier', + 'party': '_Test Supplier USD', + 'paid_to': '_Test Payable USD - _TC', + 'paid_from': 'Cash - _TC', + 'paid_amount': 70000, + 'target_exchange_rate': 70, + 'received_amount': 1000, + }) + pay.insert() + pay.submit() + + pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", + conversion_rate=75, rate=500, do_not_save=1, qty=1) + pi.cost_center = "_Test Cost Center - _TC" + pi.advances = [] + pi.append("advances", { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 1000, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70 + }) + pi.save() + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 37500.0], + ["_Test Payable USD - _TC", -40000.0], + ["Exchange Gain/Loss - _TC", 2500.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s + group by account order by account asc""", (pi.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", + conversion_rate=73, rate=500, do_not_save=1, qty=1) + pi_2.cost_center = "_Test Cost Center - _TC" + pi_2.advances = [] + pi_2.append("advances", { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 500, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70 + }) + pi_2.save() + pi_2.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 36500.0], + ["_Test Payable USD - _TC", -38000.0], + ["Exchange Gain/Loss - _TC", 1500.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s + group by account order by account asc""", (pi_2.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + expected_gle = [ + ["_Test Payable USD - _TC", 70000.0], + ["Cash - _TC", -70000.0] + ] + + gl_entries = frappe.db.sql(""" + select account, sum(debit - credit) as balance from `tabGL Entry` + where voucher_no=%s and is_cancelled=0 + group by account order by account asc""", (pay.name), as_dict=1) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.balance) + + pi.reload() + pi.cancel() + + pi_2.reload() + pi_2.cancel() + + pay.reload() + pay.cancel() + + frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled) + def test_purchase_invoice_advance_taxes(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry diff --git a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json index 5801b17f66..63dfff8921 100644 --- a/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json +++ b/erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -1,235 +1,127 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-03-08 15:36:46", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "creation": "2013-03-08 15:36:46", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "remarks", + "reference_row", + "col_break1", + "advance_amount", + "allocated_amount", + "exchange_gain_loss", + "ref_exchange_rate" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Type", - "length": 0, - "no_copy": 1, - "oldfieldname": "journal_voucher", - "oldfieldtype": "Link", - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "180px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "no_copy": 1, + "oldfieldname": "journal_voucher", + "oldfieldtype": "Link", + "options": "DocType", + "print_width": "180px", + "read_only": 1, "width": "180px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Reference Name", - "length": 0, - "no_copy": 1, - "options": "reference_type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "no_copy": 1, + "options": "reference_type", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "remarks", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Remarks", - "length": 0, - "no_copy": 1, - "oldfieldname": "remarks", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 3, + "fieldname": "remarks", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Remarks", + "no_copy": 1, + "oldfieldname": "remarks", + "oldfieldtype": "Small Text", + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_row", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Row", - "length": 0, - "no_copy": 1, - "oldfieldname": "jv_detail_no", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "80px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_row", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Row", + "no_copy": 1, + "oldfieldname": "jv_detail_no", + "oldfieldtype": "Date", + "print_hide": 1, + "print_width": "80px", + "read_only": 1, "width": "80px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "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": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "advance_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Advance Amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "advance_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "advance_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Advance Amount", + "no_copy": 1, + "oldfieldname": "advance_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "100px", + "read_only": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Allocated Amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "allocated_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated Amount", + "no_copy": 1, + "oldfieldname": "allocated_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "100px", "width": "100px" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "ref_exchange_rate", + "fieldtype": "Float", + "label": "Reference Exchange Rate", + "non_negative": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2016-08-26 02:30:54.407138", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Invoice Advance", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-04-20 16:26:53.820530", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Invoice Advance", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 55a5b99907..6d1f6249c1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -840,6 +840,7 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) + self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.allocate_advance_taxes(gl_entries) diff --git a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json index 14bf4d8133..29422d68cf 100644 --- a/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json +++ b/erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json @@ -1,235 +1,128 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:27:41", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "creation": "2013-02-22 01:27:41", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "remarks", + "reference_row", + "col_break1", + "advance_amount", + "allocated_amount", + "exchange_gain_loss", + "ref_exchange_rate" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Type", - "length": 0, - "no_copy": 1, - "oldfieldname": "journal_voucher", - "oldfieldtype": "Link", - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "250px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "no_copy": 1, + "oldfieldname": "journal_voucher", + "oldfieldtype": "Link", + "options": "DocType", + "print_width": "250px", + "read_only": 1, "width": "250px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Reference Name", - "length": 0, - "no_copy": 1, - "options": "reference_type", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "no_copy": 1, + "options": "reference_type", + "print_hide": 1, + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "remarks", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Remarks", - "length": 0, - "no_copy": 1, - "oldfieldname": "remarks", - "oldfieldtype": "Small Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 3, + "fieldname": "remarks", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Remarks", + "no_copy": 1, + "oldfieldname": "remarks", + "oldfieldtype": "Small Text", + "print_width": "150px", + "read_only": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_row", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Reference Row", - "length": 0, - "no_copy": 1, - "oldfieldname": "jv_detail_no", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "reference_row", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Row", + "no_copy": 1, + "oldfieldname": "jv_detail_no", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "120px", + "read_only": 1, "width": "120px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "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": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "advance_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Advance amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "advance_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "advance_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Advance amount", + "no_copy": 1, + "oldfieldname": "advance_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "120px", + "read_only": 1, "width": "120px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Allocated amount", - "length": 0, - "no_copy": 1, - "oldfieldname": "allocated_amount", - "oldfieldtype": "Currency", - "options": "party_account_currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "120px", - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "columns": 2, + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated amount", + "no_copy": 1, + "oldfieldname": "allocated_amount", + "oldfieldtype": "Currency", + "options": "party_account_currency", + "print_width": "120px", "width": "120px" + }, + { + "fieldname": "exchange_gain_loss", + "fieldtype": "Currency", + "label": "Exchange Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "ref_exchange_rate", + "fieldtype": "Float", + "label": "Reference Exchange Rate", + "non_negative": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "menu_index": 0, - "modified": "2016-08-26 02:36:10.718057", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Invoice Advance", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-06-04 20:25:49.832052", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Invoice Advance", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 66a9b60125..a8e4b153f8 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -472,7 +472,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate + "exchange_rate": d.exchange_rate if not d.exchange_gain_loss else payment_entry.get_exchange_rate(), + "exchange_gain_loss": d.exchange_gain_loss # only populated from invoice in case of advance allocation } if d.voucher_detail_no: @@ -498,12 +499,15 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): payment_entry.set_amounts() if d.difference_amount and d.difference_account: - payment_entry.set_gain_or_loss(account_details={ + account_details = { 'account': d.difference_account, 'cost_center': payment_entry.cost_center or frappe.get_cached_value('Company', - payment_entry.company, "cost_center"), - 'amount': d.difference_amount - }) + payment_entry.company, "cost_center") + } + if d.difference_amount: + account_details['amount'] = d.difference_amount + + payment_entry.set_gain_or_loss(account_details=account_details) if not do_not_save: payment_entry.save(ignore_permissions=True) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1c086e9edc..a9860ed2f0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -124,6 +124,8 @@ class AccountsController(TransactionBase): if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() + self.set_advance_gain_or_loss() + if self.is_return: self.validate_qty() else: @@ -584,15 +586,18 @@ class AccountsController(TransactionBase): allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) - self.append("advances", { + advance_row = { "doctype": self.doctype + " Advance", "reference_type": d.reference_type, "reference_name": d.reference_name, "reference_row": d.reference_row, "remarks": d.remarks, "advance_amount": flt(d.amount), - "allocated_amount": allocated_amount - }) + "allocated_amount": allocated_amount, + "ref_exchange_rate": flt(d.exchange_rate) # exchange_rate of advance entry + } + + self.append("advances", advance_row) def get_advance_entries(self, include_unallocated=True): if self.doctype == "Sales Invoice": @@ -650,6 +655,66 @@ class AccountsController(TransactionBase): "Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.") .format(d.reference_name, d.against_order)) + def set_advance_gain_or_loss(self): + if not self.get("advances"): + return + + for d in self.get("advances"): + advance_exchange_rate = d.ref_exchange_rate + if (d.allocated_amount and self.conversion_rate != 1 + and self.conversion_rate != advance_exchange_rate): + + base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount + base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount + difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate + + d.exchange_gain_loss = difference + + def make_exchange_gain_loss_gl_entries(self, gl_entries): + if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: + for d in self.get("advances"): + if d.exchange_gain_loss: + party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer + party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to + party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer" + + gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') + account_currency = get_account_currency(gain_loss_account) + if account_currency != self.company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) + + # for purchase + dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' + # just reverse for sales? + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + + gl_entries.append( + self.get_gl_dict({ + "account": gain_loss_account, + "account_currency": account_currency, + "against": party, + dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center, + "project": self.project + }, item=d) + ) + + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + + gl_entries.append( + self.get_gl_dict({ + "account": party_account, + "party_type": party_type, + "party": party, + "against": gain_loss_account, + dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), + dr_or_cr: abs(d.exchange_gain_loss), + "cost_center": self.cost_center, + "project": self.project + }, self.party_account_currency, item=self) + ) + def update_against_document_in_jv(self): """ Links invoice and advance voucher: @@ -690,7 +755,9 @@ class AccountsController(TransactionBase): if self.party_account_currency != self.company_currency else 1), 'grand_total': (self.base_grand_total if self.party_account_currency == self.company_currency else self.grand_total), - 'outstanding_amount': self.outstanding_amount + 'outstanding_amount': self.outstanding_amount, + 'difference_account': frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account'), + 'exchange_gain_loss': flt(d.get('exchange_gain_loss')) }) lst.append(args) @@ -1289,6 +1356,8 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, party_account_field = "paid_from" if party_type == "Customer" else "paid_to" currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" payment_type = "Receive" if party_type == "Customer" else "Pay" + exchange_rate_field = "source_exchange_rate" if payment_type == "Receive" else "target_exchange_rate" + payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" @@ -1305,27 +1374,28 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, "Payment Entry" as reference_type, t1.name as reference_name, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency + t1.{0} as currency, t1.{4} as exchange_rate from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 where t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 and t2.reference_doctype = %s {2} order by t1.posting_date {3} - """.format(currency_field, party_account_field, reference_condition, limit_cond), + """.format(currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1) if include_unallocated: unallocated_payment_entries = frappe.db.sql(""" select "Payment Entry" as reference_type, name as reference_name, - remarks, unallocated_amount as amount + remarks, unallocated_amount as amount, {2} as exchange_rate from `tabPayment Entry` where {0} = %s and party_type = %s and party = %s and payment_type = %s and docstatus = 1 and unallocated_amount > 0 order by posting_date {1} - """.format(party_account_field, limit_cond), (party_account, party_type, party, payment_type), as_dict=1) + """.format(party_account_field, limit_cond, exchange_rate_field), + (party_account, party_type, party, payment_type), as_dict=1) return list(payment_entries_against_order) + list(unallocated_payment_entries) From 0683337f146b583425be369b5e0035439f601b76 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Jul 2021 20:21:07 +0530 Subject: [PATCH 429/550] fix: incorrect response by variance & resolution by variance (#26173) --- erpnext/patches.txt | 1 + .../v13_0/update_response_by_variance.py | 31 +++++++++++++++ .../service_level_agreement.js | 4 +- .../service_level_agreement.json | 2 +- .../service_level_agreement.py | 4 +- .../test_service_level_agreement.py | 38 ++++++++++++++++++- .../service_level_priority.json | 8 ++-- 7 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 erpnext/patches/v13_0/update_response_by_variance.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 986b0c5711..c93f7a7ed9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -290,5 +290,6 @@ erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold +erpnext.patches.v13_0.update_response_by_variance erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details diff --git a/erpnext/patches/v13_0/update_response_by_variance.py b/erpnext/patches/v13_0/update_response_by_variance.py new file mode 100644 index 0000000000..ef4d976383 --- /dev/null +++ b/erpnext/patches/v13_0/update_response_by_variance.py @@ -0,0 +1,31 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists('DocType', 'Issue') and frappe.db.count('Issue'): + invalid_issues = frappe.get_all('Issue', { + 'first_responded_on': ['is', 'set'], + 'response_by_variance': ['<', 0] + }, ["name", "response_by_variance", "timestampdiff(Second, `first_responded_on`, `response_by`) as variance"]) + + # issues which has response_by_variance set as -ve + # but diff between first_responded_on & response_by is +ve i.e SLA isn't failed + invalid_issues = [d for d in invalid_issues if d.get('variance') > 0] + + for issue in invalid_issues: + frappe.db.set_value('Issue', issue.get('name'), 'response_by_variance', issue.get('variance'), update_modified=False) + + invalid_issues = frappe.get_all('Issue', { + 'resolution_date': ['is', 'set'], + 'resolution_by_variance': ['<', 0] + }, ["name", "resolution_by_variance", "timestampdiff(Second, `resolution_date`, `resolution_by`) as variance"]) + + # issues which has resolution_by_variance set as -ve + # but diff between resolution_date & resolution_by is +ve i.e SLA isn't failed + invalid_issues = [d for d in invalid_issues if d.get('variance') > 0] + + for issue in invalid_issues: + frappe.db.set_value('Issue', issue.get('name'), 'resolution_by_variance', issue.get('variance'), update_modified=False) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js index 308bce48df..ae2080c3b5 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.js +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.js @@ -5,15 +5,15 @@ frappe.ui.form.on('Service Level Agreement', { setup: function(frm) { if (cint(frm.doc.apply_sla_for_resolution) === 1) { frm.get_field('priorities').grid.editable_fields = [ - {fieldname: 'priority', columns: 1}, {fieldname: 'default_priority', columns: 1}, + {fieldname: 'priority', columns: 2}, {fieldname: 'response_time', columns: 2}, {fieldname: 'resolution_time', columns: 2} ]; } else { frm.get_field('priorities').grid.editable_fields = [ - {fieldname: 'priority', columns: 1}, {fieldname: 'default_priority', columns: 1}, + {fieldname: 'priority', columns: 2}, {fieldname: 'response_time', columns: 3}, ]; } 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 de3389aa42..ef14b29896 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -1,6 +1,6 @@ { "actions": [], - "autoname": "format:SLA-{document_type}-{service_level}-{####}", + "autoname": "format:SLA-{document_type}-{service_level}", "creation": "2018-12-26 21:08:15.448812", "doctype": "DocType", "editable_grid": 1, diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 60e5fbe80e..8739cb2364 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -797,7 +797,7 @@ def set_response_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("response_by"): doc.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - if meta.has_field("response_by_variance"): + if meta.has_field("response_by_variance") and not doc.get('first_responded_on'): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) doc.response_by_variance = round(time_diff_in_seconds(doc.response_by, now_time), 2) @@ -805,7 +805,7 @@ def set_resolution_by_and_variance(doc, meta, start_date_time, priority): if meta.has_field("resolution_by"): doc.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - if meta.has_field("resolution_by_variance"): + if meta.has_field("resolution_by_variance") and not doc.get("resolution_date"): now_time = frappe.flags.current_time or now_datetime(doc.get("owner")) doc.resolution_by_variance = round(time_diff_in_seconds(doc.resolution_by, now_time), 2) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 7c18a6577f..865fadc97c 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -217,6 +217,42 @@ class TestServiceLevelAgreement(unittest.TestCase): lead.reload() self.assertEqual(lead.agreement_status, 'Fulfilled') + def test_changing_of_variance_after_response(self): + # create lead + doctype = "Lead" + lead_sla = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + entity_type=None, entity=None, + response_time=14400, + doctype=doctype, + sla_fulfilled_on=[{"status": "Replied"}], + apply_sla_for_resolution=0 + ) + creation = datetime.datetime(2019, 3, 4, 12, 0) + lead = make_lead(creation=creation, index=2) + self.assertEqual(lead.service_level_agreement, lead_sla.name) + + # set lead as replied to set first responded on + frappe.flags.current_time = datetime.datetime(2019, 3, 4, 15, 30) + lead.reload() + lead.status = 'Replied' + lead.save() + lead.reload() + self.assertEqual(lead.agreement_status, 'Fulfilled') + + # check response_by_variance + self.assertEqual(lead.first_responded_on, frappe.flags.current_time) + self.assertEqual(lead.response_by_variance, 1800.0) + + # make a change on the document & + # check response_by_variance is unchanged + frappe.flags.current_time = datetime.datetime(2019, 3, 4, 18, 30) + lead.status = 'Open' + lead.save() + lead.reload() + self.assertEqual(lead.response_by_variance, 1800.0) + def tearDown(self): for d in frappe.get_all("Service Level Agreement"): frappe.delete_doc("Service Level Agreement", d.name, force=1) @@ -249,7 +285,7 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "doctype": "Service Level Agreement", "enabled": 1, "document_type": doctype, - "service_level": "__Test Service Level", + "service_level": "__Test {} SLA".format(entity_type if entity_type else "Default"), "default_service_level_agreement": default_service_level_agreement, "default_priority": "Medium", "holiday_list": holiday_list, diff --git a/erpnext/support/doctype/service_level_priority/service_level_priority.json b/erpnext/support/doctype/service_level_priority/service_level_priority.json index 0367fc6d88..b410fe6660 100644 --- a/erpnext/support/doctype/service_level_priority/service_level_priority.json +++ b/erpnext/support/doctype/service_level_priority/service_level_priority.json @@ -5,9 +5,9 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "priority", - "cb_01", "default_priority", + "cb_01", + "priority", "sb_00", "response_time", "cb_00", @@ -15,7 +15,7 @@ ], "fields": [ { - "columns": 1, + "columns": 2, "fieldname": "priority", "fieldtype": "Link", "in_list_view": 1, @@ -64,7 +64,7 @@ ], "istable": 1, "links": [], - "modified": "2021-05-29 19:52:51.733248", + "modified": "2021-06-21 12:00:58.089962", "modified_by": "Administrator", "module": "Support", "name": "Service Level Priority", From 9965af166e5e03899fc0629ec8d6835f5f7b6cdd Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 16 Jun 2021 19:03:27 +0530 Subject: [PATCH 430/550] feat: details fetched from supplier group in supplier --- erpnext/buying/doctype/supplier/supplier.js | 13 +++++++++++++ erpnext/buying/doctype/supplier/supplier.py | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 4ddc458175..af6401b3fe 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Supplier Group Details'), function () { + frm.trigger("get_supplier_group_details"); + }, __('Actions')); + // indicators erpnext.utils.set_party_dashboard_indicators(frm); } }, + get_supplier_group_details: function(frm) { + frappe.call({ + method: "get_supplier_group_details", + doc: frm.doc, + callback: function(r){ + frm.refresh() + } + }); + }, is_internal_supplier: function(frm) { if (frm.doc.is_internal_supplier == 1) { diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index edeb135d95..791f71ed3b 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -51,6 +51,23 @@ class Supplier(TransactionBase): validate_party_accounts(self) self.validate_internal_supplier() + @frappe.whitelist() + def get_supplier_group_details(self): + doc = frappe.get_doc('Supplier Group', self.supplier_group) + self.payment_terms = "" + self.accounts = [] + + if not self.accounts and doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + self.save() + + if not self.payment_terms and doc.payment_terms: + self.payment_terms = doc.payment_terms + + def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") @@ -86,4 +103,4 @@ class Supplier(TransactionBase): create_contact(supplier, 'Supplier', doc.name, args.get('supplier_email_' + str(i))) except frappe.NameError: - pass \ No newline at end of file + pass From 1cb2af00a84534983cf086fef7a9118e0ecb10b6 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 17 Jun 2021 15:48:55 +0530 Subject: [PATCH 431/550] feat: details fetched from customer group in customer --- erpnext/selling/doctype/customer/customer.js | 17 ++++++++++++- erpnext/selling/doctype/customer/customer.py | 26 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 825b170a90..91944adef3 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -130,6 +130,10 @@ frappe.ui.form.on("Customer", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Customer Group Details'), function () { + frm.trigger("get_customer_group_details"); + }, __('Actions')); + // indicator erpnext.utils.set_party_dashboard_indicators(frm); @@ -145,4 +149,15 @@ frappe.ui.form.on("Customer", { if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name); }, -}); \ No newline at end of file + get_customer_group_details: function(frm) { + frappe.call({ + method: "get_customer_group_details", + doc: frm.doc, + callback: function(r){ + frm.refresh() + } + }); + + } +}); + diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 818888c0c1..cdeb089618 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -78,6 +78,32 @@ class Customer(TransactionBase): if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) + @frappe.whitelist() + def get_customer_group_details(self): + doc = frappe.get_doc('Customer Group', self.customer_group) + self.accounts = self.credit_limits = [] + self.payment_terms = self.default_price_list = "" + + if not self.accounts and doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + self.save() + + if not self.credit_limits and doc.credit_limits: + for credit in doc.credit_limits: + child = self.append('credit_limits') + child.company = credit.company + child.credit_limit = credit.credit_limit + self.save() + + if not self.payment_terms and doc.payment_terms: + self.payment_terms = doc.payment_terms + + if not self.default_price_list and doc.default_price_list: + self.default_price_list = doc.default_price_list + def check_customer_group_change(self): frappe.flags.customer_group_changed = False From f07f7e9305d70a02a931cde8a720e8c8682ed2b4 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 18 Jun 2021 18:53:28 +0530 Subject: [PATCH 432/550] test: test case for fetching supplier group details --- .../buying/doctype/supplier/test_supplier.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index f9c8d35518..faa813aa4c 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -13,6 +13,26 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): + def test_get_supplier_group_details(self): + doc = frappe.get_doc("Supplier Group", "Local") + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + doc.append("accounts", test_account_details) + doc.save() + doc = frappe.get_doc("Supplier", "_Test Supplier") + doc.supplier_group = "Local" + doc.payment_terms = "" + doc.accounts = [] + doc.save() + doc.get_supplier_group_details() + self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(doc.accounts[0].company, "_Test Company") + self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date frappe.db.set_value( @@ -136,4 +156,4 @@ def create_supplier(**args): return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Supplier", args.supplier_name) \ No newline at end of file + return frappe.get_doc("Supplier", args.supplier_name) From fedee0e8da2f0a4f60bbd06fbb22ff4d46e7def6 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 18 Jun 2021 19:13:18 +0530 Subject: [PATCH 433/550] test: test cases for fetching customer group details --- .../selling/doctype/customer/test_customer.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7761aa70fb..8cb07aaa8a 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -27,6 +27,38 @@ class TestCustomer(unittest.TestCase): def tearDown(self): set_credit_limit('_Test Customer', '_Test Company', 0) + def test_get_customer_group_details(self): + doc = frappe.get_doc("Customer Group", "Commercial") + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + doc.default_price_list = "Standard Buying" + doc.credit_limits = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + test_credit_limits = { + "company": "_Test Company", + "credit_limit": 350000 + } + doc.append("accounts", test_account_details) + doc.append("credit_limits", test_credit_limits) + doc.save() + + doc = frappe.get_doc("Customer", "_Test Customer") + doc.customer_group = "Commercial" + doc.payment_terms = doc.default_price_list = "" + doc.accounts = doc.credit_limits= [] + doc.save() + doc.get_customer_group_details() + self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + + self.assertEqual(doc.accounts[0].company, "_Test Company") + self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + + self.assertEqual(doc.credit_limits[0].company, "_Test Company") + self.assertEqual(doc.credit_limits[0].credit_limit, 350000 ) + def test_party_details(self): from erpnext.accounts.party import get_party_details From dd0a8f20e27555d59c52c25f87dbae450b29321c Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 2 Jul 2021 19:35:50 +0530 Subject: [PATCH 434/550] test: updated test cases --- .../buying/doctype/supplier/test_supplier.py | 24 ++++++++------- .../selling/doctype/customer/test_customer.py | 30 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index faa813aa4c..8980466270 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -14,7 +14,8 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): def test_get_supplier_group_details(self): - doc = frappe.get_doc("Supplier Group", "Local") + doc = frappe.new_doc("Supplier Group") + doc.supplier_group_name = "_Testing Supplier Group" doc.payment_terms = "_Test Payment Term Template 3" doc.accounts = [] test_account_details = { @@ -23,15 +24,18 @@ class TestSupplier(unittest.TestCase): } doc.append("accounts", test_account_details) doc.save() - doc = frappe.get_doc("Supplier", "_Test Supplier") - doc.supplier_group = "Local" - doc.payment_terms = "" - doc.accounts = [] - doc.save() - doc.get_supplier_group_details() - self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(doc.accounts[0].company, "_Test Company") - self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + s_doc = frappe.new_doc("Supplier") + s_doc.supplier_name = "Testing Supplier" + s_doc.supplier_group = "_Testing Supplier Group" + s_doc.payment_terms = "" + s_doc.accounts = [] + s_doc.insert() + s_doc.get_supplier_group_details() + self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(s_doc.accounts[0].company, "_Test Company") + self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") + s_doc.delete() + doc.delete() def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 8cb07aaa8a..b1a5b52f96 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -28,7 +28,8 @@ class TestCustomer(unittest.TestCase): set_credit_limit('_Test Customer', '_Test Company', 0) def test_get_customer_group_details(self): - doc = frappe.get_doc("Customer Group", "Commercial") + doc = frappe.new_doc("Customer Group") + doc.customer_group_name = "_Testing Customer Group" doc.payment_terms = "_Test Payment Term Template 3" doc.accounts = [] doc.default_price_list = "Standard Buying" @@ -43,21 +44,24 @@ class TestCustomer(unittest.TestCase): } doc.append("accounts", test_account_details) doc.append("credit_limits", test_credit_limits) - doc.save() + doc.insert() - doc = frappe.get_doc("Customer", "_Test Customer") - doc.customer_group = "Commercial" - doc.payment_terms = doc.default_price_list = "" - doc.accounts = doc.credit_limits= [] - doc.save() - doc.get_customer_group_details() - self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + c_doc = frappe.new_doc("Customer") + c_doc.customer_name = "Testing Customer" + c_doc.customer_group = "_Testing Customer Group" + c_doc.payment_terms = c_doc.default_price_list = "" + c_doc.accounts = c_doc.credit_limits= [] + c_doc.insert() + c_doc.get_customer_group_details() + self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(doc.accounts[0].company, "_Test Company") - self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + self.assertEqual(c_doc.accounts[0].company, "_Test Company") + self.assertEqual(c_doc.accounts[0].account, "Creditors - _TC") - self.assertEqual(doc.credit_limits[0].company, "_Test Company") - self.assertEqual(doc.credit_limits[0].credit_limit, 350000 ) + self.assertEqual(c_doc.credit_limits[0].company, "_Test Company") + self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000) + c_doc.delete() + doc.delete() def test_party_details(self): from erpnext.accounts.party import get_party_details From 1e8f598ba5afdf1efce64de40f058c337ce580b5 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 2 Jul 2021 22:02:07 +0530 Subject: [PATCH 435/550] fix: Sider --- erpnext/buying/doctype/supplier/supplier.js | 4 ++-- erpnext/selling/doctype/customer/customer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index af6401b3fe..1766c2c80c 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -72,8 +72,8 @@ frappe.ui.form.on("Supplier", { frappe.call({ method: "get_supplier_group_details", doc: frm.doc, - callback: function(r){ - frm.refresh() + callback: function() { + frm.refresh(); } }); }, diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 91944adef3..2849466267 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -153,8 +153,8 @@ frappe.ui.form.on("Customer", { frappe.call({ method: "get_customer_group_details", doc: frm.doc, - callback: function(r){ - frm.refresh() + callback: function() { + frm.refresh(); } }); From 74b3fc1e1cba75e61eb6e3d97051f3be29f1370c Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 12 Jul 2021 09:18:19 +0530 Subject: [PATCH 436/550] refactor: suggested changes --- erpnext/buying/doctype/supplier/supplier.py | 6 ++-- erpnext/selling/doctype/customer/customer.py | 29 +++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 791f71ed3b..fd16b23c22 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -57,16 +57,16 @@ class Supplier(TransactionBase): self.payment_terms = "" self.accounts = [] - if not self.accounts and doc.accounts: + if doc.accounts: for account in doc.accounts: child = self.append('accounts') child.company = account.company child.account = account.account - self.save() - if not self.payment_terms and doc.payment_terms: + if doc.payment_terms: self.payment_terms = doc.payment_terms + self.save() def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index cdeb089618..3b62081e24 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -84,25 +84,22 @@ class Customer(TransactionBase): self.accounts = self.credit_limits = [] self.payment_terms = self.default_price_list = "" - if not self.accounts and doc.accounts: - for account in doc.accounts: - child = self.append('accounts') - child.company = account.company - child.account = account.account - self.save() + tables = [["accounts", "account"], ["credit_limits", "credit_limit"]] + fields = ["payment_terms", "default_price_list"] - if not self.credit_limits and doc.credit_limits: - for credit in doc.credit_limits: - child = self.append('credit_limits') - child.company = credit.company - child.credit_limit = credit.credit_limit - self.save() + for row in tables: + table, field = row[0], row[1] + if not doc.get(table): continue - if not self.payment_terms and doc.payment_terms: - self.payment_terms = doc.payment_terms + for entry in doc.get(table): + child = self.append(table) + child.update({"company": entry.company, field: entry.get(field)}) - if not self.default_price_list and doc.default_price_list: - self.default_price_list = doc.default_price_list + for field in fields: + if not doc.get(field): continue + self.update({field: doc.get(field)}) + + self.save() def check_customer_group_change(self): frappe.flags.customer_group_changed = False From a8b6cf8114e512048fcfb0a47bea1d1662d566ae Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 13 Jul 2021 11:45:34 +0530 Subject: [PATCH 437/550] fix: bank remittance report issue (#26398) --- erpnext/payroll/report/bank_remittance/bank_remittance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py index 500543ceb0..05a5366a5c 100644 --- a/erpnext/payroll/report/bank_remittance/bank_remittance.py +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py @@ -95,6 +95,7 @@ def execute(filters=None): "amount": salary.net_pay, } data.append(row) + return columns, data def get_bank_accounts(): @@ -116,7 +117,7 @@ def get_payroll_entries(accounts, filters): entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"]) payment_accounts = [d.payment_account for d in entries] - set_company_account(payment_accounts, entries) + entries = set_company_account(payment_accounts, entries) return entries def get_salary_slips(payroll_entries): From 7c5711ddf4a447b4c1d9f3522060ad84486ddd4f Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 13 Jul 2021 14:09:06 +0530 Subject: [PATCH 438/550] fix: pos item cart dom updates (#26459) --- .../selling/page/point_of_sale/pos_item_cart.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 38508c219b..f7b2c1d93c 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -965,8 +965,23 @@ erpnext.PointOfSale.ItemCart = class { }); } + attach_refresh_field_event(frm) { + $(frm.wrapper).off('refresh-fields'); + $(frm.wrapper).on('refresh-fields', () => { + if (frm.doc.items.length) { + frm.doc.items.forEach(item => { + this.update_item_html(item); + }); + } + this.update_totals_section(frm); + }); + } + load_invoice() { const frm = this.events.get_frm(); + + this.attach_refresh_field_event(frm); + this.fetch_customer_details(frm.doc.customer).then(() => { this.events.customer_details_updated(this.customer_info); this.update_customer_section(); From 621927d9f94a7c080c24b6b194f9b58f17cee6d4 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 13 Jul 2021 14:13:39 +0530 Subject: [PATCH 439/550] fix: move the rename abbreviation job to long queue (#26462) --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 915e6a4f31..36a7d20a8f 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -395,7 +395,7 @@ class Company(NestedSet): @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue="long", company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) From 5265ba39f0be132db770fcaae527afc945f54b16 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Tue, 13 Jul 2021 14:58:17 +0530 Subject: [PATCH 440/550] feat: Added fields for dispatch address in Sales Order, Sales Invoice, Delivery Note for Eway Bill --- .../doctype/sales_invoice/sales_invoice.json | 19 +++++++++++++++++- erpnext/regional/india/utils.py | 10 ++++++---- .../doctype/sales_order/sales_order.json | 20 ++++++++++++++++++- erpnext/selling/sales_common.js | 8 ++++++-- .../doctype/delivery_note/delivery_note.json | 19 +++++++++++++++++- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index e7dd6b8a60..0a9a105b7c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -48,6 +48,8 @@ "shipping_address", "company_address", "company_address_display", + "dispatch_address_name", + "dispatch_address", "currency_and_price_list", "currency", "conversion_rate", @@ -1966,6 +1968,21 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -1978,7 +1995,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-05-20 22:48:33.988881", + "modified": "2021-07-08 14:03:55.502522", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 81c0918b99..61f5a0578e 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -431,9 +431,11 @@ def get_ewb_data(dt, dn): company_address = frappe.get_doc('Address', doc.company_address) billing_address = frappe.get_doc('Address', doc.customer_address) + #added dispatch address + dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) shipping_address = frappe.get_doc('Address', doc.shipping_address_name) - data = get_address_details(data, doc, company_address, billing_address) + data = get_address_details(data, doc, company_address, billing_address, dispatch_address) data.itemList = [] data.totalValue = doc.total @@ -519,10 +521,10 @@ def get_gstins_for_company(company): `tabDynamic Link`.link_name = %(company)s""", {"company": company}) return company_gstins -def get_address_details(data, doc, company_address, billing_address): +def get_address_details(data, doc, company_address, billing_address, dispatch_address): data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') - data.fromStateCode = data.actualFromStateCode = validate_state_code( - company_address.gst_state_number, 'Company Address') + data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address') + data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Company Address') if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: data.toGstin = 'URP' diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 762b6f1d6c..d31db820ab 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -38,6 +38,8 @@ "col_break46", "shipping_address_name", "shipping_address", + "dispatch_address_name", + "dispatch_address", "customer_group", "territory", "currency_and_price_list", @@ -1486,13 +1488,29 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "allow_on_submit": 1, + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "dispatch_address_name", + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-04-15 23:55:13.439068", + "modified": "2021-07-08 21:37:44.177493", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index eb02867720..f515baf31b 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -26,7 +26,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } }; }); - } + } setup_queries() { var me = this; @@ -85,7 +85,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran refresh() { super.refresh(); - + frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} this.frm.toggle_display("customer_name", @@ -114,6 +114,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name"); } + dispatch_address_name() { + erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address"); + } + sales_partner() { this.apply_pricing_rule(); } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index f20e76f5bf..dbfeb4a10b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -32,6 +32,8 @@ "contact_info", "shipping_address_name", "shipping_address", + "dispatch_address_name", + "dispatch_address", "contact_person", "contact_display", "contact_mobile", @@ -1282,13 +1284,28 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "fieldname": "dispatch_address_name", + "fieldtype": "Link", + "label": "Dispatch Address Name", + "options": "Address", + "print_hide": 1 + }, + { + "depends_on": "dispatch_address_name", + "fieldname": "dispatch_address", + "fieldtype": "Small Text", + "label": "Dispatch Address", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2021-06-11 19:27:30.901112", + "modified": "2021-07-08 21:37:20.802652", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From a4eba7a4090338b2a2b805c90261538baaae4f3e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 13 Jul 2021 15:34:25 +0530 Subject: [PATCH 441/550] fix: show child item group items on portal --- erpnext/setup/doctype/item_group/item_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 1a83cb62dd..c46b6cc9bd 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -87,8 +87,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): if not field_filters: field_filters = {} - # Ensure the query remains within current item group - field_filters['item_group'] = self.name + # Ensure the query remains within current item group & sub group + field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)] engine = ProductQuery() context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) From 4acbeecbbe631764d12bf1e92f4f0379133c58d4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 13 Jul 2021 11:45:41 +0530 Subject: [PATCH 442/550] fix: multi-currency issue --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++- erpnext/stock/get_item_details.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c32a8a95a1..9da461f497 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -713,7 +713,8 @@ def get_bom_item_rate(args, bom_doc): "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function "conversion_factor": args.get("conversion_factor") or 1, "plc_conversion_rate": 1, - "ignore_party": True + "ignore_party": True, + "ignore_conversion_rate": True }) item_doc = frappe.get_cached_doc("Item", args.get("item_code")) out = frappe._dict() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index ca174a3f63..4657700dbb 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -441,7 +441,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t if item_tax_templates is None: item_tax_templates = {} - + if item_rates is None: item_rates = {} @@ -807,10 +807,14 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate - if (not args.conversion_rate - and args.currency==frappe.get_cached_value('Company', args.company, "default_currency")): + company_currency = frappe.get_cached_value('Company', args.company, "default_currency") + if (not args.conversion_rate and args.currency==company_currency): args.conversion_rate = 1.0 + if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency): + args.conversion_rate = get_exchange_rate(args.currency, + company_currency, args.transaction_date, "for_buying") or 1.0 + # validate currency conversion rate validate_conversion_rate(args.currency, args.conversion_rate, meta.get_label("conversion_rate"), args.company) From 07d9f3f74ba83ec6d7851c78f7881af0429fe960 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 21:17:17 +0530 Subject: [PATCH 443/550] fix: Incorrect discount amount on amended document --- erpnext/public/js/controllers/taxes_and_totals.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1de9ec1a7d..52efbb5f6c 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -67,6 +67,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ calculate_discount_amount: function(){ if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { + this.calculate_item_values(); + this.calculate_net_total(); this.set_discount_amount(); this.apply_discount_amount(); } From fc9714b871e4ad19bc8b6e5744352f41e9c74b0b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Jul 2021 10:06:38 +0530 Subject: [PATCH 444/550] fix: Unable to download GSTR-1 json --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 10961593e1..cfcb8c3444 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -584,7 +584,7 @@ class Gstr1Report(object): def get_json(filters, report_name, data): filters = json.loads(filters) report_data = json.loads(data) - gstin = get_company_gstin_number(filters["company"], filters["company_address"]) + gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address")) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) From 0bad696faf6e20bfcd8809cb3db56c4f503a438f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Jul 2021 10:06:38 +0530 Subject: [PATCH 445/550] fix: Unable to download GSTR-1 json --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 10961593e1..cfcb8c3444 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -584,7 +584,7 @@ class Gstr1Report(object): def get_json(filters, report_name, data): filters = json.loads(filters) report_data = json.loads(data) - gstin = get_company_gstin_number(filters["company"], filters["company_address"]) + gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address")) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) From 7be9f8dab16a54a020d1e37541eb0392457c3bc9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Jul 2021 20:23:52 +0530 Subject: [PATCH 446/550] fix: Error on creation of company for India --- erpnext/regional/india/setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5f9d5ed0d6..5ef04b66c7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -122,10 +122,12 @@ def add_print_formats(): def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] + sales_invoice_series = frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") + ['SINV-.YY.-', 'SRET-.YY.-', ''] + purchase_invoice_series = frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") + ['PINV-.YY.-', 'PRET-.YY.-', ''] if not patch: - make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') - make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') + make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') + make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '') make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') def make_custom_fields(update=True): From fea29ae8cb8190bf1f043dc14e997e2d20c02356 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 12 Jul 2021 18:29:52 +0530 Subject: [PATCH 447/550] fix: Use update flag for company dependant fixtures --- erpnext/regional/india/setup.py | 11 +++++++---- erpnext/setup/doctype/company/company.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5ef04b66c7..92654608da 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -12,7 +12,10 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today def setup(company=None, patch=True): - setup_company_independent_fixtures(patch=patch) + # Company independent fixtures should be called only once at the first company setup + if frappe.db.count('Company', {'country': 'India'}) <=1: + setup_company_independent_fixtures(patch=patch) + if not patch: make_fixtures(company) @@ -122,8 +125,8 @@ def add_print_formats(): def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] - sales_invoice_series = frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") + ['SINV-.YY.-', 'SRET-.YY.-', ''] - purchase_invoice_series = frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") + ['PINV-.YY.-', 'PRET-.YY.-', ''] + sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") + purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") if not patch: make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') @@ -788,7 +791,7 @@ def set_tax_withholding_category(company): doc.flags.ignore_mandatory = True doc.insert() else: - doc = frappe.get_doc("Tax Withholding Category", d.get("name")) + doc = frappe.get_doc("Tax Withholding Category", d.get("name"), for_update=True) if accounts: doc.append("accounts", accounts[0]) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 36a7d20a8f..8755125c81 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -291,7 +291,7 @@ class Company(NestedSet): cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') if cash and self.default_cash_account \ and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): - mode_of_payment = frappe.get_doc('Mode of Payment', cash) + mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True) mode_of_payment.append('accounts', { 'company': self.name, 'default_account': self.default_cash_account From f661d82c0039e049abd34937319b5b3b7c446b19 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 18:04:24 +0530 Subject: [PATCH 448/550] fix: Unallocated amount in Payment Entry after taxes --- .../doctype/payment_entry/payment_entry.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ff00fde523..3591fcdc70 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -411,9 +411,15 @@ class PaymentEntry(AccountsController): if not self.advance_tax_account: frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - reference_doclist = [] net_total = self.paid_amount - included_in_paid_amount = 0 + + for reference in self.get("references"): + net_total_for_tds = 0 + if reference.reference_doctype == 'Purchase Order': + net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) + + if net_total_for_tds: + net_total = net_total_for_tds # Adding args as purchase invoice to get TDS amount args = frappe._dict({ @@ -430,7 +436,6 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ - 'included_in_paid_amount': included_in_paid_amount, 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -516,18 +521,17 @@ class PaymentEntry(AccountsController): self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): - self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ - and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ - and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): - self.unallocated_amount = (self.received_amount_after_tax + total_deductions - + and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ + and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): + self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate elif self.payment_type == "Pay" \ - and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \ - and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate): - self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions + + and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ + and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): + self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate def set_difference_amount(self): @@ -537,11 +541,11 @@ class PaymentEntry(AccountsController): base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount_after_tax + self.difference_amount = base_party_amount - self.base_received_amount elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount_after_tax - base_party_amount + self.difference_amount = self.base_paid_amount - base_party_amount else: - self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) @@ -690,8 +694,8 @@ class PaymentEntry(AccountsController): "account": self.paid_from, "account_currency": self.paid_from_account_currency, "against": self.party if self.payment_type=="Pay" else self.paid_to, - "credit_in_account_currency": self.paid_amount_after_tax, - "credit": self.base_paid_amount_after_tax, + "credit_in_account_currency": self.paid_amount, + "credit": self.base_paid_amount, "cost_center": self.cost_center }, item=self) ) @@ -701,8 +705,8 @@ class PaymentEntry(AccountsController): "account": self.paid_to, "account_currency": self.paid_to_account_currency, "against": self.party if self.payment_type=="Receive" else self.paid_from, - "debit_in_account_currency": self.received_amount_after_tax, - "debit": self.base_received_amount_after_tax, + "debit_in_account_currency": self.received_amount, + "debit": self.base_received_amount, "cost_center": self.cost_center }, item=self) ) @@ -715,15 +719,17 @@ class PaymentEntry(AccountsController): if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" + against = self.party or self.paid_from elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" + against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() gl_entries.append( self.get_gl_dict({ "account": d.account_head, - "against": self.party if self.payment_type=="Receive" else self.paid_from, + "against": against, dr_or_cr: d.base_tax_amount, dr_or_cr + "_in_account_currency": d.base_tax_amount if account_currency==self.company_currency @@ -735,14 +741,12 @@ class PaymentEntry(AccountsController): gl_entries.append( self.get_gl_dict({ "account": payment_or_advance_account, - "against": self.party if self.payment_type=="Receive" else self.paid_from, + "against": against, dr_or_cr: -1 * d.base_tax_amount, dr_or_cr + "_in_account_currency": -1*d.base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, - "party_type": self.party_type, - "party": self.party }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -767,9 +771,9 @@ class PaymentEntry(AccountsController): if self.advance_tax_account: return self.advance_tax_account elif self.payment_type == 'Receive': - return self.paid_from - elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to + elif self.payment_type in ('Pay', 'Internal Transfer'): + return self.paid_from def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: @@ -1648,12 +1652,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) - if dt == "Purchase Order" and doc.apply_tds: - if party_account_currency == bank.account_currency: - paid_amount = received_amount = doc.base_net_total - else: - paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) - return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): From 5cc4a201cb650a37e211d717a44fab7c373dc0d0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 18:52:12 +0530 Subject: [PATCH 449/550] fix: Hide amount after tax fields --- .../accounts/doctype/payment_entry/payment_entry.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 51f18a5a4e..6f362c1fbb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -667,6 +667,7 @@ { "fieldname": "base_paid_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Paid Amount After Tax (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 @@ -693,21 +694,25 @@ "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax", - "options": "paid_to_account_currency" + "options": "paid_to_account_currency", + "read_only": 1 }, { "depends_on": "doc.received_amount", "fieldname": "base_received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax (Company Currency)", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-22 20:37:06.154206", + "modified": "2021-07-09 08:58:15.008761", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 5179f85e63f82b9aa98e0b1f168b8ba883fd22eb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 20:00:55 +0530 Subject: [PATCH 450/550] fix: Remove unintentional changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3591fcdc70..1b39862c5c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -528,6 +528,7 @@ class PaymentEntry(AccountsController): and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + print(self.unallocated_amount, "#@#@#@#@#") elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): From 7c634f66ca3a9aa5a582810bf3922fe57d5505ad Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 20:08:29 +0530 Subject: [PATCH 451/550] fix: Remove unintentional changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1b39862c5c..2ce161d15d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -521,6 +521,7 @@ class PaymentEntry(AccountsController): self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): + self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ @@ -528,7 +529,6 @@ class PaymentEntry(AccountsController): and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate - print(self.unallocated_amount, "#@#@#@#@#") elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): From 754f432d97bea3c64c604adf60f14389d02b67aa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 12 Jul 2021 22:11:57 +0530 Subject: [PATCH 452/550] fix: Deduct included taxes from unallocated amount --- .../doctype/payment_entry/payment_entry.py | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2ce161d15d..0bc3d94d2c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -524,16 +524,19 @@ class PaymentEntry(AccountsController): self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() if self.payment_type == "Receive" \ and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + self.unallocated_amount -= included_taxes elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate + self.unallocated_amount -= included_taxes def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) @@ -549,10 +552,22 @@ class PaymentEntry(AccountsController): self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() - self.difference_amount = flt(self.difference_amount - total_deductions, + self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")) + def get_included_taxes(self): + included_taxes = 0 + for tax in self.get('taxes'): + if tax.included_in_paid_amount: + if tax.add_deduct_tax == 'Add': + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount + + return included_taxes + # Paid amount is auto allocated in the reference document by default. # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast def clear_unallocated_reference_document_rows(self): @@ -726,6 +741,10 @@ class PaymentEntry(AccountsController): against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() + tax_amount = d.tax_amount + + if self.advance_tax_account: + tax_amount = -1* tax_amount gl_entries.append( self.get_gl_dict({ @@ -739,16 +758,17 @@ class PaymentEntry(AccountsController): }, account_currency, item=d)) #Intentionally use -1 to get net values in party account - gl_entries.append( - self.get_gl_dict({ - "account": payment_or_advance_account, - "against": against, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount - if account_currency==self.company_currency - else d.tax_amount, - "cost_center": self.cost_center, - }, account_currency, item=d)) + if not d.included_in_paid_amount or self.advance_tax_account: + gl_entries.append( + self.get_gl_dict({ + "account": payment_or_advance_account, + "against": against, + dr_or_cr: -1 * d.base_tax_amount, + dr_or_cr + "_in_account_currency": -1*d.base_tax_amount + if account_currency==self.company_currency + else d.tax_amount, + "cost_center": self.cost_center, + }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): for d in self.get("deductions"): From 4478c547bfcc604d505c8b2589009c82d26c2fc0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Jul 2021 11:22:55 +0530 Subject: [PATCH 453/550] fix: Unallocated amount for inclusive charges --- .../accounts/doctype/payment_entry/payment_entry.py | 13 ++++++++----- erpnext/controllers/accounts_controller.py | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0bc3d94d2c..46904f7c57 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -436,6 +436,7 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ + 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -742,16 +743,18 @@ class PaymentEntry(AccountsController): payment_or_advance_account = self.get_party_account_for_taxes() tax_amount = d.tax_amount + base_tax_amount = d.base_tax_amount if self.advance_tax_account: - tax_amount = -1* tax_amount + tax_amount = -1 * tax_amount + base_tax_amount = -1 * base_tax_amount gl_entries.append( self.get_gl_dict({ "account": d.account_head, "against": against, - dr_or_cr: d.base_tax_amount, - dr_or_cr + "_in_account_currency": d.base_tax_amount + dr_or_cr: tax_amount, + dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": d.cost_center @@ -763,8 +766,8 @@ class PaymentEntry(AccountsController): self.get_gl_dict({ "account": payment_or_advance_account, "against": against, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount + dr_or_cr: -1 * tax_amount, + dr_or_cr + "_in_account_currency": -1 * base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a9860ed2f0..4c313c43a7 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -818,11 +818,11 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(tax.account_head) if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - else: dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + else: + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" party = self.supplier if self.doctype == "Purchase Invoice" else self.customer unallocated_amount = tax.tax_amount - tax.allocated_amount From 51ae46f0de7f41c3693de885e0bc6b472249b031 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 18:04:24 +0530 Subject: [PATCH 454/550] fix: Unallocated amount in Payment Entry after taxes --- .../doctype/payment_entry/payment_entry.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0c21aae944..20c97cf251 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -404,9 +404,15 @@ class PaymentEntry(AccountsController): if not self.advance_tax_account: frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - reference_doclist = [] net_total = self.paid_amount - included_in_paid_amount = 0 + + for reference in self.get("references"): + net_total_for_tds = 0 + if reference.reference_doctype == 'Purchase Order': + net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) + + if net_total_for_tds: + net_total = net_total_for_tds # Adding args as purchase invoice to get TDS amount args = frappe._dict({ @@ -423,7 +429,6 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ - 'included_in_paid_amount': included_in_paid_amount, 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -509,18 +514,17 @@ class PaymentEntry(AccountsController): self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): - self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ - and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ - and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): - self.unallocated_amount = (self.received_amount_after_tax + total_deductions - + and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ + and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): + self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate elif self.payment_type == "Pay" \ - and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \ - and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate): - self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions + + and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ + and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): + self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate def set_difference_amount(self): @@ -530,11 +534,11 @@ class PaymentEntry(AccountsController): base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount_after_tax + self.difference_amount = base_party_amount - self.base_received_amount elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount_after_tax - base_party_amount + self.difference_amount = self.base_paid_amount - base_party_amount else: - self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) @@ -683,8 +687,8 @@ class PaymentEntry(AccountsController): "account": self.paid_from, "account_currency": self.paid_from_account_currency, "against": self.party if self.payment_type=="Pay" else self.paid_to, - "credit_in_account_currency": self.paid_amount_after_tax, - "credit": self.base_paid_amount_after_tax, + "credit_in_account_currency": self.paid_amount, + "credit": self.base_paid_amount, "cost_center": self.cost_center }, item=self) ) @@ -694,8 +698,8 @@ class PaymentEntry(AccountsController): "account": self.paid_to, "account_currency": self.paid_to_account_currency, "against": self.party if self.payment_type=="Receive" else self.paid_from, - "debit_in_account_currency": self.received_amount_after_tax, - "debit": self.base_received_amount_after_tax, + "debit_in_account_currency": self.received_amount, + "debit": self.base_received_amount, "cost_center": self.cost_center }, item=self) ) @@ -708,15 +712,17 @@ class PaymentEntry(AccountsController): if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" + against = self.party or self.paid_from elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" + against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() gl_entries.append( self.get_gl_dict({ "account": d.account_head, - "against": self.party if self.payment_type=="Receive" else self.paid_from, + "against": against, dr_or_cr: d.base_tax_amount, dr_or_cr + "_in_account_currency": d.base_tax_amount if account_currency==self.company_currency @@ -728,14 +734,12 @@ class PaymentEntry(AccountsController): gl_entries.append( self.get_gl_dict({ "account": payment_or_advance_account, - "against": self.party if self.payment_type=="Receive" else self.paid_from, + "against": against, dr_or_cr: -1 * d.base_tax_amount, dr_or_cr + "_in_account_currency": -1*d.base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, - "party_type": self.party_type, - "party": self.party }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -760,9 +764,9 @@ class PaymentEntry(AccountsController): if self.advance_tax_account: return self.advance_tax_account elif self.payment_type == 'Receive': - return self.paid_from - elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to + elif self.payment_type in ('Pay', 'Internal Transfer'): + return self.paid_from def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: @@ -1634,12 +1638,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) - if dt == "Purchase Order" and doc.apply_tds: - if party_account_currency == bank.account_currency: - paid_amount = received_amount = doc.base_net_total - else: - paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) - return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): From 9513d61a50ea3792d627f76fd92a0fb87f00a793 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 18:52:12 +0530 Subject: [PATCH 455/550] fix: Hide amount after tax fields --- .../accounts/doctype/payment_entry/payment_entry.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 51f18a5a4e..6f362c1fbb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -667,6 +667,7 @@ { "fieldname": "base_paid_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Paid Amount After Tax (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 @@ -693,21 +694,25 @@ "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax", - "options": "paid_to_account_currency" + "options": "paid_to_account_currency", + "read_only": 1 }, { "depends_on": "doc.received_amount", "fieldname": "base_received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax (Company Currency)", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-22 20:37:06.154206", + "modified": "2021-07-09 08:58:15.008761", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 2f350bf4507eafb5d7dc25ee26f4a68f2214ac1a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 20:00:55 +0530 Subject: [PATCH 456/550] fix: Remove unintentional changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 20c97cf251..e7feebacf8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -521,6 +521,7 @@ class PaymentEntry(AccountsController): and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + print(self.unallocated_amount, "#@#@#@#@#") elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): From 01c89eaad9b94559ec4e9e200d6a5a7901e66a46 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 20:08:29 +0530 Subject: [PATCH 457/550] fix: Remove unintentional changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e7feebacf8..7f53ca91d5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -514,6 +514,7 @@ class PaymentEntry(AccountsController): self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): + self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ @@ -521,7 +522,6 @@ class PaymentEntry(AccountsController): and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate - print(self.unallocated_amount, "#@#@#@#@#") elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): From 740d5c6c5309bc19003b159c0848157f78d0bcf7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 12 Jul 2021 22:11:57 +0530 Subject: [PATCH 458/550] fix: Deduct included taxes from unallocated amount --- .../doctype/payment_entry/payment_entry.py | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7f53ca91d5..835601e534 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -517,16 +517,19 @@ class PaymentEntry(AccountsController): self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() if self.payment_type == "Receive" \ and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + self.unallocated_amount -= included_taxes elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate + self.unallocated_amount -= included_taxes def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) @@ -542,10 +545,22 @@ class PaymentEntry(AccountsController): self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() - self.difference_amount = flt(self.difference_amount - total_deductions, + self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")) + def get_included_taxes(self): + included_taxes = 0 + for tax in self.get('taxes'): + if tax.included_in_paid_amount: + if tax.add_deduct_tax == 'Add': + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount + + return included_taxes + # Paid amount is auto allocated in the reference document by default. # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast def clear_unallocated_reference_document_rows(self): @@ -719,6 +734,10 @@ class PaymentEntry(AccountsController): against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() + tax_amount = d.tax_amount + + if self.advance_tax_account: + tax_amount = -1* tax_amount gl_entries.append( self.get_gl_dict({ @@ -732,16 +751,17 @@ class PaymentEntry(AccountsController): }, account_currency, item=d)) #Intentionally use -1 to get net values in party account - gl_entries.append( - self.get_gl_dict({ - "account": payment_or_advance_account, - "against": against, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount - if account_currency==self.company_currency - else d.tax_amount, - "cost_center": self.cost_center, - }, account_currency, item=d)) + if not d.included_in_paid_amount or self.advance_tax_account: + gl_entries.append( + self.get_gl_dict({ + "account": payment_or_advance_account, + "against": against, + dr_or_cr: -1 * d.base_tax_amount, + dr_or_cr + "_in_account_currency": -1*d.base_tax_amount + if account_currency==self.company_currency + else d.tax_amount, + "cost_center": self.cost_center, + }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): for d in self.get("deductions"): From c00d851a88d8dd0f326de765c031e5790a341fbd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Jul 2021 11:22:55 +0530 Subject: [PATCH 459/550] fix: Unallocated amount for inclusive charges --- .../accounts/doctype/payment_entry/payment_entry.py | 13 ++++++++----- erpnext/controllers/accounts_controller.py | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 835601e534..7f665db2f9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -429,6 +429,7 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ + 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -735,16 +736,18 @@ class PaymentEntry(AccountsController): payment_or_advance_account = self.get_party_account_for_taxes() tax_amount = d.tax_amount + base_tax_amount = d.base_tax_amount if self.advance_tax_account: - tax_amount = -1* tax_amount + tax_amount = -1 * tax_amount + base_tax_amount = -1 * base_tax_amount gl_entries.append( self.get_gl_dict({ "account": d.account_head, "against": against, - dr_or_cr: d.base_tax_amount, - dr_or_cr + "_in_account_currency": d.base_tax_amount + dr_or_cr: tax_amount, + dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": d.cost_center @@ -756,8 +759,8 @@ class PaymentEntry(AccountsController): self.get_gl_dict({ "account": payment_or_advance_account, "against": against, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount + dr_or_cr: -1 * tax_amount, + dr_or_cr + "_in_account_currency": -1 * base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1c086e9edc..5d30b65a1e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -751,11 +751,11 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(tax.account_head) if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - else: dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + else: + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" party = self.supplier if self.doctype == "Purchase Invoice" else self.customer unallocated_amount = tax.tax_amount - tax.allocated_amount From 2a9726b09f544c54c6788be21fc7a4613467a6c7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Apr 2021 23:04:39 +0530 Subject: [PATCH 460/550] feat(India): Separate Input and Output GST tax accounts --- .../setup_wizard/data/country_wise_tax.json | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index daaa626a81..ca0bb48e78 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1229,35 +1229,70 @@ ] } ], - "*": [ + "sales_tax_templates": [ { - "title": "In State GST", + "title": "Output GST In-state", "taxes": [ { "account_head": { - "account_name": "SGST", + "account_name": "Output Tax SGST", "tax_rate": 9.00 } }, { "account_head": { - "account_name": "CGST", + "account_name": "Output Tax CGST", "tax_rate": 9.00 } } ] }, { - "title": "Out of State GST", + "title": "Output GST Out-state", "taxes": [ { "account_head": { - "account_name": "IGST", + "account_name": "Output Tax IGST", "tax_rate": 18.00 } } ] + } + ], + "purchase_tax_templates": [ + { + "title": "Input GST In-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax SGST", + "tax_rate": 9.00, + "root_type": "Asset" + } + }, + { + "account_head": { + "account_name": "Input Tax CGST", + "tax_rate": 9.00, + "root_type": "Asset" + } + } + ] }, + { + "title": "Input GST Out-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax IGST", + "tax_rate": 18.00, + "root_type": "Asset" + } + } + ] + } + ], + "*": [ { "title": "VAT 5%", "taxes": [ @@ -1349,7 +1384,7 @@ "Italy VAT 4%":{ "account_name": "IVA 4%", "tax_rate": 4.00 - } + } }, "Ivory Coast": { From de8c6eb0da0a3f159a43831763be8fcd0673002c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Apr 2021 23:23:07 +0530 Subject: [PATCH 461/550] fix: Item Tax templates for GST --- .../setup_wizard/data/country_wise_tax.json | 148 +++++++++++++++++- 1 file changed, 142 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index ca0bb48e78..7822096259 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1168,29 +1168,165 @@ "*": { "item_tax_templates": [ { - "title": "In State GST", + "title": "GST 9%", "taxes": [ { "tax_type": { - "account_name": "SGST", + "account_name": "Output Tax SGST", "tax_rate": 9.00 } }, { "tax_type": { - "account_name": "CGST", + "account_name": "Output Tax CGST", "tax_rate": 9.00 } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 18.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 18.00 + } } ] }, { - "title": "Out of State GST", + "title": "GST 5%", "taxes": [ { "tax_type": { - "account_name": "IGST", - "tax_rate": 18.00 + "account_name": "Output Tax SGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Output Tax CGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 5.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 2.5 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 5.0 + } + } + ] + }, + { + "title": "GST 12%", + "taxes": [ + { + "tax_type": { + "account_name": "Output Tax SGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax CGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 12.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 6.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 12.0 + } + } + ] + }, + { + "title": "GST 28%", + "taxes": [ + { + "tax_type": { + "account_name": "Output Tax SGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax CGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Output Tax IGST", + "tax_rate": 28.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax SGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST", + "tax_rate": 14.0 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST", + "tax_rate": 28.0 } } ] From 3031535a241f055371094590bff2a6df006ea324 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Apr 2021 11:49:02 +0530 Subject: [PATCH 462/550] fix: Add tax categories on company setup --- .../setup_wizard/data/country_wise_tax.json | 22 +++++++++++++++++ .../setup_wizard/operations/taxes_setup.py | 24 +++++++++++-------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 7822096259..14215f030f 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1166,6 +1166,28 @@ "India": { "chart_of_accounts": { "*": { + "tax_categories": [ + { + "title": "In-Sate", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Out-Sate", + "is_inter_state": 1, + "gst_state": "" + }, + { + "title": "Reverse Charge", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Registered Composition", + "is_inter_state": 0, + "gst_state": "" + } + ], "item_tax_templates": [ { "title": "GST 9%", diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index f4fe18e116..974ef5eaa7 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -77,16 +77,11 @@ def simple_to_detailed(templates): def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts') - coa_data = data.get('chart_of_accounts', {}) - tax_templates = coa_data.get(coa_name) or coa_data.get('*', {}) - tax_categories = data.get('tax_categories') - sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*', {}) - purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*', {}) - item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*', {}) - - if tax_categories: - for tax_category in tax_categories: - make_tax_catgory(tax_category) + tax_templates = data.get(coa_name) or data.get('*') + sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*') + purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*') + item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*') + tax_categories = tax_templates.get('tax_categories') if sales_tax_templates: for template in sales_tax_templates: @@ -100,6 +95,10 @@ def from_detailed_data(company_name, data): for template in item_tax_templates: make_item_tax_template(company_name, template) + if tax_categories: + for tax_category in tax_categories: + make_tax_category(tax_category) + def make_taxes_and_charges_template(company_name, doctype, template): template['company'] = company_name @@ -158,6 +157,11 @@ def make_item_tax_template(company_name, template): return frappe.get_doc(template).insert(ignore_permissions=True) +def make_tax_category(tax_category): + """ Make tax category based on title if not already created """ + doctype = 'Tax Category' + if not frappe.db.exists(doctype, tax_category) + frappe.get_doc(tax_category).insert(ignore_permissions=True) def get_or_create_account(company_name, account): """ From e166c264b49ae717f204658b604cd219a3fc71db Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Apr 2021 20:41:57 +0530 Subject: [PATCH 463/550] fix: Update country-wise-tax JSON and tax setup --- .../setup_wizard/data/country_wise_tax.json | 34 ++++++++++++------- .../setup_wizard/operations/taxes_setup.py | 22 ++++++------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 14215f030f..7a61538b0e 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1168,12 +1168,12 @@ "*": { "tax_categories": [ { - "title": "In-Sate", + "title": "In-State", "is_inter_state": 0, "gst_state": "" }, { - "title": "Out-Sate", + "title": "Out-State", "is_inter_state": 1, "gst_state": "" }, @@ -1394,16 +1394,19 @@ { "account_head": { "account_name": "Output Tax SGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "account_type": "Tax" } }, { "account_head": { "account_name": "Output Tax CGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "account_type": "Tax" } } - ] + ], + "tax_category": "In-State" }, { "title": "Output GST Out-state", @@ -1411,10 +1414,12 @@ { "account_head": { "account_name": "Output Tax IGST", - "tax_rate": 18.00 + "tax_rate": 18.00, + "account_type": "Tax" } } - ] + ], + "tax_category": "Out-State" } ], "purchase_tax_templates": [ @@ -1425,17 +1430,20 @@ "account_head": { "account_name": "Input Tax SGST", "tax_rate": 9.00, - "root_type": "Asset" + "root_type": "Asset", + "account_type": "Tax" } }, { "account_head": { "account_name": "Input Tax CGST", "tax_rate": 9.00, - "root_type": "Asset" + "root_type": "Asset", + "account_type": "Tax" } } - ] + ], + "tax_category": "In-State" }, { "title": "Input GST Out-state", @@ -1444,10 +1452,12 @@ "account_head": { "account_name": "Input Tax IGST", "tax_rate": 18.00, - "root_type": "Asset" + "root_type": "Asset", + "account_type": "Tax" } } - ] + ], + "tax_category": "Out-State" } ], "*": [ diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 974ef5eaa7..59f1e58078 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -83,6 +83,10 @@ def from_detailed_data(company_name, data): item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*') tax_categories = tax_templates.get('tax_categories') + if tax_categories: + for tax_category in tax_categories: + make_tax_category(tax_category) + if sales_tax_templates: for template in sales_tax_templates: make_taxes_and_charges_template(company_name, 'Sales Taxes and Charges Template', template) @@ -95,10 +99,6 @@ def from_detailed_data(company_name, data): for template in item_tax_templates: make_item_tax_template(company_name, template) - if tax_categories: - for tax_category in tax_categories: - make_tax_category(tax_category) - def make_taxes_and_charges_template(company_name, doctype, template): template['company'] = company_name @@ -160,8 +160,9 @@ def make_item_tax_template(company_name, template): def make_tax_category(tax_category): """ Make tax category based on title if not already created """ doctype = 'Tax Category' - if not frappe.db.exists(doctype, tax_category) - frappe.get_doc(tax_category).insert(ignore_permissions=True) + if not frappe.db.exists(doctype, tax_category): + tax_category['doctype'] = doctype + frappe.get_doc(tax_category).insert(ignore_permissions=True) def get_or_create_account(company_name, account): """ @@ -173,12 +174,13 @@ def get_or_create_account(company_name, account): existing_accounts = frappe.get_list('Account', filters={ - 'company': company_name, - 'root_type': root_type + 'account_name': account.get('account_name'), + 'account_number': account.get('account_number', '') }, or_filters={ - 'account_name': account.get('account_name'), - 'account_number': account.get('account_number') + 'company': company_name, + 'root_type': root_type, + 'is_group': 0 } ) From a90e5fd2e1213605a08a495149274f63a5a7b047 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Apr 2021 20:42:20 +0530 Subject: [PATCH 464/550] fix: Issues on new company setup --- erpnext/regional/india/setup.py | 4 ++-- .../regional/report/e_invoice_summary/e_invoice_summary.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 3e0b9b733b..31e936d410 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -731,12 +731,12 @@ def set_tax_withholding_category(company): docs = get_tds_details(accounts, fiscal_year) for d in docs: - try: + if not frappe.db.exists("Tax Withholding Category", d.get("name")): doc = frappe.get_doc(d) doc.flags.ignore_permissions = True doc.flags.ignore_mandatory = True doc.insert() - except frappe.DuplicateEntryError: + else: doc = frappe.get_doc("Tax Withholding Category", d.get("name")) if accounts: diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json index 4deb073a53..d0000ad50d 100644 --- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json @@ -11,7 +11,7 @@ "is_standard": "Yes", "json": "{}", "letter_head": "Logo", - "modified": "2021-03-12 12:36:48.689413", + "modified": "2021-03-13 12:36:48.689413", "modified_by": "Administrator", "module": "Regional", "name": "E-Invoice Summary", From 8fd2d8b5d0040fd407aeac84c1ef658ec04d6b39 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Apr 2021 16:35:52 +0530 Subject: [PATCH 465/550] fix: Ignore validations for Tax Setup --- erpnext/regional/india/setup.py | 10 ++++-- erpnext/setup/doctype/company/company.py | 2 +- .../setup_wizard/operations/taxes_setup.py | 34 ++++++++++++++----- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 31e936d410..c554630aae 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -25,6 +25,7 @@ def setup_company_independent_fixtures(patch=False): frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) create_gratuity_rule() add_print_formats() + update_accounts_settings_for_taxes() def add_hsn_sac_codes(): if frappe.flags.in_test and frappe.flags.created_hsn_codes: @@ -733,6 +734,7 @@ def set_tax_withholding_category(company): for d in docs: if not frappe.db.exists("Tax Withholding Category", d.get("name")): doc = frappe.get_doc(d) + doc.flags.ignore_validate = True doc.flags.ignore_permissions = True doc.flags.ignore_mandatory = True doc.insert() @@ -749,11 +751,12 @@ def set_tax_withholding_category(company): doc.append("rates", d.get('rates')[0]) doc.flags.ignore_permissions = True + doc.flags.ignore_validdate = True doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True doc.save() def set_tds_account(docs, company): - abbr = frappe.get_value("Company", company, "abbr") parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company}) if parent_account: docs.extend([ @@ -912,7 +915,6 @@ def get_tds_details(accounts, fiscal_year): ] def create_gratuity_rule(): - # Standard Indain Gratuity Rule if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"): rule = frappe.new_doc("Gratuity Rule") @@ -930,3 +932,7 @@ def create_gratuity_rule(): rule.flags.ignore_mandatory = True rule.save() + +def update_accounts_settings_for_taxes(): + if frappe.db.count('Company') == 1: + frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8fd905d7a7..8fc0cbefa1 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -110,7 +110,7 @@ class Company(NestedSet): self.create_default_warehouses() if frappe.flags.country_change: - install_country_fixtures(self.name) + install_country_fixtures(self.name, self.country) self.create_default_tax_template() if not frappe.db.get_value("Department", {"company": self.name}): diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 59f1e58078..6da3f386f8 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -129,8 +129,11 @@ def make_taxes_and_charges_template(company_name, doctype, template): if fieldname not in tax_row: tax_row[fieldname] = default_value - return frappe.get_doc(template).insert(ignore_permissions=True) - + doc = frappe.get_doc(template) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True) + return doc def make_item_tax_template(company_name, template): """Create an Item Tax Template. @@ -155,14 +158,21 @@ def make_item_tax_template(company_name, template): if 'tax_rate' not in tax_row: tax_row['tax_rate'] = account_data.get('tax_rate') - return frappe.get_doc(template).insert(ignore_permissions=True) + doc = frappe.get_doc(template) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True) + return doc def make_tax_category(tax_category): """ Make tax category based on title if not already created """ doctype = 'Tax Category' if not frappe.db.exists(doctype, tax_category): tax_category['doctype'] = doctype - frappe.get_doc(tax_category).insert(ignore_permissions=True) + doc = frappe.get_doc(tax_category) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True) def get_or_create_account(company_name, account): """ @@ -175,7 +185,8 @@ def get_or_create_account(company_name, account): existing_accounts = frappe.get_list('Account', filters={ 'account_name': account.get('account_name'), - 'account_number': account.get('account_number', '') + 'account_number': account.get('account_number', ''), + 'company': company_name }, or_filters={ 'company': company_name, @@ -197,8 +208,11 @@ def get_or_create_account(company_name, account): account['root_type'] = root_type account['is_group'] = 0 - return frappe.get_doc(account).insert(ignore_permissions=True, ignore_mandatory=True) - + doc = frappe.get_doc(account) + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.insert(ignore_permissions=True, ignore_mandatory=True) + return doc def get_or_create_tax_group(company_name, root_type): # Look for a group account of type 'Tax' @@ -243,7 +257,11 @@ def get_or_create_tax_group(company_name, root_type): 'account_type': 'Tax', 'account_name': account_name, 'parent_account': root_account.name - }).insert(ignore_permissions=True) + }) + + tax_group_account.flags.ignore_links = True + tax_group_account.flags.ignore_validate = True + tax_group_account.insert(ignore_permissions=True) tax_group_name = tax_group_account.name From 8ed1afd93db4d70917cf7d3520565c40e8f25790 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 12:22:16 +0530 Subject: [PATCH 466/550] fix: Remove redundant get_doc --- erpnext/regional/india/setup.py | 2 +- erpnext/setup/doctype/company/company.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index c554630aae..95fc6801cc 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -681,7 +681,7 @@ def make_custom_fields(update=True): def make_fixtures(company=None): docs = [] - company = company.name if company else frappe.db.get_value("Global Defaults", None, "default_company") + company = company or frappe.db.get_value("Global Defaults", None, "default_company") set_salary_components(docs) set_tds_account(docs, company) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8fc0cbefa1..d112e8eef9 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -440,13 +440,12 @@ def get_name_with_abbr(name, company): return " - ".join(parts) -def install_country_fixtures(company): - company_doc = frappe.get_doc("Company", company) - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(company_doc.country)) +def install_country_fixtures(company, country): + path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: - module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(company_doc.country)) - frappe.get_attr(module_name)(company_doc, False) + module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country)) + frappe.get_attr(module_name)(company, False) except Exception as e: frappe.log_error() frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country))) From 1001f2978404eb11772f96859e3724d199a2fc08 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 12:23:11 +0530 Subject: [PATCH 467/550] fix: Gracefully handle duplicate bank account name to make setup faster --- erpnext/accounts/doctype/account/account.py | 6 +++++ .../chart_of_accounts/chart_of_accounts.py | 18 ------------- erpnext/public/js/setup_wizard.js | 26 ------------------- 3 files changed, 6 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 1be2fbf5c8..645f49bcdc 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -28,6 +28,12 @@ class Account(NestedSet): from erpnext.accounts.utils import get_autoname_with_number self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) + def before_insert(self): + # Update Bank account name if conflicting with any other account + if frappe.flags.in_install and self.account_type == 'Bank': + if frappe.db.get_value('Account', {'account_name': self.account_name}): + self.account_name = self.account_name + '-1' + def validate(self): from erpnext.accounts.utils import validate_field_number if frappe.local.flags.allow_unverified_charts: diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 927adc7086..9b6842d896 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -188,24 +188,6 @@ def build_account_tree(tree, parent, all_accounts): # call recursively to build a subtree for current account build_account_tree(tree[child.account_name], child, all_accounts) -@frappe.whitelist() -def validate_bank_account(coa, bank_account): - accounts = [] - chart = get_chart(coa) - - if chart: - def _get_account_names(account_master): - for account_name, child in iteritems(account_master): - if account_name not in ["account_number", "account_type", - "root_type", "is_group", "tax_rate"]: - accounts.append(account_name) - - _get_account_names(child) - - _get_account_names(chart) - - return (bank_account in accounts) - @frappe.whitelist() def build_tree_from_json(chart_template, chart_data=None): ''' get chart template from its folder and parse the json to be rendered as tree ''' diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index ef03b01698..a3045724fe 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -139,36 +139,10 @@ erpnext.setup.slides_settings = [ }, validate: function () { - let me = this; - let exist; - if (!this.validate_fy_dates()) { return false; } - // Validate bank name - if(me.values.bank_account){ - frappe.call({ - async: false, - method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", - args: { - "coa": me.values.chart_of_accounts, - "bank_account": me.values.bank_account - }, - callback: function (r) { - if(r.message){ - exist = r.message; - me.get_field("bank_account").set_value(""); - let message = __('Account {0} already exists. Please enter a different name for your bank account.', - [me.values.bank_account] - ); - frappe.msgprint(message); - } - } - }); - return !exist; // Return False if exist = true - } - return true; }, From 96300fa7915e86ef6b782c3dffbf19dedef5991f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 18:22:29 +0530 Subject: [PATCH 468/550] fix: Add validation for GST Settings --- .../regional/doctype/gst_settings/gst_settings.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.py b/erpnext/regional/doctype/gst_settings/gst_settings.py index bc956e9fa8..af3d92e59a 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.py +++ b/erpnext/regional/doctype/gst_settings/gst_settings.py @@ -19,6 +19,21 @@ class GSTSettings(Document): from tabAddress where country = "India" and ifnull(gstin, '')!='' ''') self.set_onload('data', data) + def validate(self): + # Validate duplicate accounts + self.validate_duplicate_accounts() + + def validate_duplicate_accounts(self): + account_list = [] + for account in self.get('gst_accounts'): + for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']: + if account.get(fieldname) in account_list: + frappe.throw(_("Account {0} appears multiple times").format( + frappe.bold(account.get(fieldname)))) + + if account.get(fieldname): + account_list.append(account.get(fieldname)) + @frappe.whitelist() def send_reminder(): frappe.has_permission('GST Settings', throw=True) From 0380cca3b7fbada0af784905bb859948c9b4e95a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 2 May 2021 22:29:48 +0530 Subject: [PATCH 469/550] fix: Add GST accounts to GST Settings --- erpnext/regional/india/setup.py | 52 ++++++++++++++++++++++++ erpnext/setup/doctype/company/company.py | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 95fc6801cc..8ccbc54c5e 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -15,6 +15,7 @@ def setup(company=None, patch=True): setup_company_independent_fixtures(patch=patch) if not patch: make_fixtures(company) + setup_gst_settings(company) # TODO: for all countries def setup_company_independent_fixtures(patch=False): @@ -699,6 +700,57 @@ def make_fixtures(company=None): # create records for Tax Withholding Category set_tax_withholding_category(company) +def setup_gst_settings(company): + # Will only add default GST accounts if present + input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] + output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] + gst_settings = frappe.get_single('GST Settings') + existing_account_list = [] + + for account in gst_settings.get('gst_accounts'): + for key in ['cgst_account', 'sgst_account', 'igst_account']: + existing_account_list.append(account.get(key)) + + gst_accounts = frappe._dict(frappe.get_all("Account", + {'company': company, 'name': ('like', "%GST%")}, ['account_name', 'name'], as_list=1)) + + all_input_account_exists = 0 + all_output_account_exists = 0 + + for account in input_account_names: + if not gst_accounts.get(account): + all_input_account_exists = 1 + + # Check if already added in GST Settings + if gst_accounts.get(account) in existing_account_list: + all_input_account_exists = 1 + + for account in output_account_names: + if not gst_accounts.get(account): + all_output_account_exists = 1 + + # Check if already added in GST Settings + if gst_accounts.get(account) in existing_account_list: + all_output_account_exists = 1 + + if not all_input_account_exists: + gst_settings.append('gst_accounts', { + 'company': company, + 'cgst_account': gst_accounts.get(input_account_names[0]), + 'sgst_account': gst_accounts.get(input_account_names[1]), + 'igst_account': gst_accounts.get(input_account_names[2]) + }) + + if not all_output_account_exists: + gst_settings.append('gst_accounts', { + 'company': company, + 'cgst_account': gst_accounts.get(output_account_names[0]), + 'sgst_account': gst_accounts.get(output_account_names[1]), + 'igst_account': gst_accounts.get(output_account_names[2]) + }) + + gst_settings.save() + def set_salary_components(docs): docs.extend([ {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index d112e8eef9..36a7d20a8f 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -448,7 +448,7 @@ def install_country_fixtures(company, country): frappe.get_attr(module_name)(company, False) except Exception as e: frappe.log_error() - frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(company_doc.country))) + frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country))) def update_company_current_month_sales(company): From a72589cb7e6ba5496f8466958a9c0ad28b66df7c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 29 May 2021 23:54:51 +0530 Subject: [PATCH 470/550] fix: Add accounts and templates for reverse charge --- erpnext/accounts/doctype/account/account.py | 6 - erpnext/accounts/utils.py | 2 +- erpnext/regional/india/setup.py | 68 +++++------ erpnext/setup/doctype/company/company.py | 2 +- .../setup_wizard/data/country_wise_tax.json | 115 +++++++++++++++++- .../setup_wizard/operations/company_setup.py | 23 ---- 6 files changed, 148 insertions(+), 68 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 645f49bcdc..1be2fbf5c8 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -28,12 +28,6 @@ class Account(NestedSet): from erpnext.accounts.utils import get_autoname_with_number self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company) - def before_insert(self): - # Update Bank account name if conflicting with any other account - if frappe.flags.in_install and self.account_type == 'Bank': - if frappe.db.get_value('Account', {'account_name': self.account_name}): - self.account_name = self.account_name + '-1' - def validate(self): from erpnext.accounts.utils import validate_field_number if frappe.local.flags.allow_unverified_charts: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a8e4b153f8..1cdbd8d38a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -788,7 +788,7 @@ def get_children(doctype, parent, company, is_root=False): return acc def create_payment_gateway_account(gateway, payment_channel="Email"): - from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account + from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account company = frappe.db.get_value("Global Defaults", None, "default_company") if not company: diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 8ccbc54c5e..39705f6e44 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -704,6 +704,7 @@ def setup_gst_settings(company): # Will only add default GST accounts if present input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] + rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM'] gst_settings = frappe.get_single('GST Settings') existing_account_list = [] @@ -712,45 +713,40 @@ def setup_gst_settings(company): existing_account_list.append(account.get(key)) gst_accounts = frappe._dict(frappe.get_all("Account", - {'company': company, 'name': ('like', "%GST%")}, ['account_name', 'name'], as_list=1)) + {'company': company, 'account_name': ('in', input_account_names + + output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1)) - all_input_account_exists = 0 - all_output_account_exists = 0 - - for account in input_account_names: - if not gst_accounts.get(account): - all_input_account_exists = 1 - - # Check if already added in GST Settings - if gst_accounts.get(account) in existing_account_list: - all_input_account_exists = 1 - - for account in output_account_names: - if not gst_accounts.get(account): - all_output_account_exists = 1 - - # Check if already added in GST Settings - if gst_accounts.get(account) in existing_account_list: - all_output_account_exists = 1 - - if not all_input_account_exists: - gst_settings.append('gst_accounts', { - 'company': company, - 'cgst_account': gst_accounts.get(input_account_names[0]), - 'sgst_account': gst_accounts.get(input_account_names[1]), - 'igst_account': gst_accounts.get(input_account_names[2]) - }) - - if not all_output_account_exists: - gst_settings.append('gst_accounts', { - 'company': company, - 'cgst_account': gst_accounts.get(output_account_names[0]), - 'sgst_account': gst_accounts.get(output_account_names[1]), - 'igst_account': gst_accounts.get(output_account_names[2]) - }) + add_accounts_in_gst_settings(company, input_account_names, gst_accounts, + existing_account_list, gst_settings) + add_accounts_in_gst_settings(company, output_account_names, gst_accounts, + existing_account_list, gst_settings) + add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts, + existing_account_list, gst_settings, is_reverse_charge=1) gst_settings.save() +def add_accounts_in_gst_settings(company, account_names, gst_accounts, + existing_account_list, gst_settings, is_reverse_charge=0): + accounts_not_added = 1 + + for account in account_names: + # Default Account Added does not exists + if not gst_accounts.get(account): + accounts_not_added = 0 + + # Check if already added in GST Settings + if gst_accounts.get(account) in existing_account_list: + accounts_not_added = 0 + + if accounts_not_added: + gst_settings.append('gst_accounts', { + 'company': company, + 'cgst_account': gst_accounts.get(account_names[0]), + 'sgst_account': gst_accounts.get(account_names[1]), + 'igst_account': gst_accounts.get(account_names[2]), + 'is_reverse_charge_account': is_reverse_charge + }) + def set_salary_components(docs): docs.extend([ {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', @@ -803,7 +799,7 @@ def set_tax_withholding_category(company): doc.append("rates", d.get('rates')[0]) doc.flags.ignore_permissions = True - doc.flags.ignore_validdate = True + doc.flags.ignore_validate = True doc.flags.ignore_mandatory = True doc.flags.ignore_links = True doc.save() diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 36a7d20a8f..0b64f0d767 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -110,8 +110,8 @@ class Company(NestedSet): self.create_default_warehouses() if frappe.flags.country_change: - install_country_fixtures(self.name, self.country) self.create_default_tax_template() + install_country_fixtures(self.name, self.country) if not frappe.db.get_value("Department", {"company": self.name}): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 7a61538b0e..6df57f1738 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1178,7 +1178,12 @@ "gst_state": "" }, { - "title": "Reverse Charge", + "title": "Reverse Charge In-State", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Reverse Charge Out-State", "is_inter_state": 0, "gst_state": "" }, @@ -1227,6 +1232,24 @@ "account_name": "Input Tax IGST", "tax_rate": 18.00 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 9.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 18.00 + } } ] }, @@ -1268,6 +1291,24 @@ "account_name": "Input Tax IGST", "tax_rate": 5.0 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 2.50 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 2.50 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 5.00 + } } ] }, @@ -1309,6 +1350,24 @@ "account_name": "Input Tax IGST", "tax_rate": 12.0 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 6.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 6.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 12.00 + } } ] }, @@ -1350,6 +1409,24 @@ "account_name": "Input Tax IGST", "tax_rate": 28.0 } + }, + { + "tax_type": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 14.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 14.00 + } + }, + { + "tax_type": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 28.00 + } } ] }, @@ -1458,6 +1535,42 @@ } ], "tax_category": "Out-State" + }, + { + "title": "Input GST RCM In-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax SGST RCM", + "tax_rate": 9.00, + "root_type": "Asset", + "account_type": "Tax" + } + }, + { + "account_head": { + "account_name": "Input Tax CGST RCM", + "tax_rate": 9.00, + "root_type": "Asset", + "account_type": "Tax" + } + } + ], + "tax_category": "Reverse Charge In-State" + }, + { + "title": "Input GST RCM Out-state", + "taxes": [ + { + "account_head": { + "account_name": "Input Tax IGST RCM", + "tax_rate": 18.00, + "root_type": "Asset", + "account_type": "Tax" + } + } + ], + "tax_category": "Reverse Charge Out-State" } ], "*": [ diff --git a/erpnext/setup/setup_wizard/operations/company_setup.py b/erpnext/setup/setup_wizard/operations/company_setup.py index 3f0bb14649..4edf9485dc 100644 --- a/erpnext/setup/setup_wizard/operations/company_setup.py +++ b/erpnext/setup/setup_wizard/operations/company_setup.py @@ -42,29 +42,6 @@ def enable_shopping_cart(args): 'quotation_series': "QTN-", }).insert() -def create_bank_account(args): - if args.get("bank_account"): - company_name = args.get('company_name') - bank_account_group = frappe.db.get_value("Account", - {"account_type": "Bank", "is_group": 1, "root_type": "Asset", - "company": company_name}) - if bank_account_group: - bank_account = frappe.get_doc({ - "doctype": "Account", - 'account_name': args.get("bank_account"), - 'parent_account': bank_account_group, - 'is_group':0, - 'company': company_name, - "account_type": "Bank", - }) - try: - return bank_account.insert() - except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account"))) - except frappe.DuplicateEntryError: - # bank account same as a CoA entry - pass - def create_email_digest(): from frappe.utils.user import get_system_managers system_managers = get_system_managers(only_name=True) From 269510b98f5c93b4ecffcbd3ca9b7d027ba38639 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Jun 2021 13:26:21 +0530 Subject: [PATCH 471/550] fix: Regional settings setup --- erpnext/regional/india/setup.py | 3 +-- erpnext/setup/doctype/company/company.py | 2 +- .../setup/setup_wizard/operations/taxes_setup.py | 14 +++++++++++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 39705f6e44..5f9d5ed0d6 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -15,7 +15,6 @@ def setup(company=None, patch=True): setup_company_independent_fixtures(patch=patch) if not patch: make_fixtures(company) - setup_gst_settings(company) # TODO: for all countries def setup_company_independent_fixtures(patch=False): @@ -700,7 +699,7 @@ def make_fixtures(company=None): # create records for Tax Withholding Category set_tax_withholding_category(company) -def setup_gst_settings(company): +def update_regional_tax_settings(country, company): # Will only add default GST accounts if present input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 0b64f0d767..36a7d20a8f 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -110,8 +110,8 @@ class Company(NestedSet): self.create_default_warehouses() if frappe.flags.country_change: - self.create_default_tax_template() install_country_fixtures(self.name, self.country) + self.create_default_tax_template() if not frappe.db.get_value("Department", {"company": self.name}): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 6da3f386f8..040256bfd1 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -26,7 +26,8 @@ def setup_taxes_and_charges(company_name: str, country: str): if 'chart_of_accounts' not in country_wise_tax: country_wise_tax = simple_to_detailed(country_wise_tax) - from_detailed_data(company_name, country_wise_tax) + from_detailed_data(company_name, country_wise_tax.get('chart_of_accounts')) + update_regional_tax_settings(country, company_name) def simple_to_detailed(templates): @@ -100,6 +101,17 @@ def from_detailed_data(company_name, data): make_item_tax_template(company_name, template) +def update_regional_tax_settings(country, company): + path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) + if os.path.exists(path.encode("utf-8")): + try: + module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country)) + frappe.get_attr(module_name)(country, company) + except Exception as e: + # Log error and ignore if failed to setup regional tax settings + frappe.log_error() + pass + def make_taxes_and_charges_template(company_name, doctype, template): template['company'] = company_name template['doctype'] = doctype From ed54e4e276980a23b60efcf20f4bd4a81100b290 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 2 Jun 2021 14:12:28 +0530 Subject: [PATCH 472/550] fix: Check for tax category --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 040256bfd1..f6cbabd439 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -179,7 +179,7 @@ def make_item_tax_template(company_name, template): def make_tax_category(tax_category): """ Make tax category based on title if not already created """ doctype = 'Tax Category' - if not frappe.db.exists(doctype, tax_category): + if not frappe.db.exists(doctype, tax_category['title']): tax_category['doctype'] = doctype doc = frappe.get_doc(tax_category) doc.flags.ignore_links = True From 87f4df80eab4c2425f03360ecff9d0108f144650 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 12:38:37 +0530 Subject: [PATCH 473/550] fix: Move tax categories up in country wise json --- .../setup_wizard/data/country_wise_tax.json | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 6df57f1738..e36bf5cbe0 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1164,35 +1164,35 @@ }, "India": { + "tax_categories": [ + { + "title": "In-State", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Out-State", + "is_inter_state": 1, + "gst_state": "" + }, + { + "title": "Reverse Charge In-State", + "is_inter_state": 0, + "gst_state": "" + }, + { + "title": "Reverse Charge Out-State", + "is_inter_state": 1, + "gst_state": "" + }, + { + "title": "Registered Composition", + "is_inter_state": 0, + "gst_state": "" + } + ], "chart_of_accounts": { "*": { - "tax_categories": [ - { - "title": "In-State", - "is_inter_state": 0, - "gst_state": "" - }, - { - "title": "Out-State", - "is_inter_state": 1, - "gst_state": "" - }, - { - "title": "Reverse Charge In-State", - "is_inter_state": 0, - "gst_state": "" - }, - { - "title": "Reverse Charge Out-State", - "is_inter_state": 0, - "gst_state": "" - }, - { - "title": "Registered Composition", - "is_inter_state": 0, - "gst_state": "" - } - ], "item_tax_templates": [ { "title": "GST 9%", From b71497067daaaa69dc7a7e12d54e7896c13f7e8f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 12:44:56 +0530 Subject: [PATCH 474/550] chore: Add comments --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index f6cbabd439..c42a0416a0 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -142,6 +142,9 @@ def make_taxes_and_charges_template(company_name, doctype, template): tax_row[fieldname] = default_value doc = frappe.get_doc(template) + + # Data in country wise json is already pre validated, hence validations can be ignored + # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) @@ -171,6 +174,9 @@ def make_item_tax_template(company_name, template): tax_row['tax_rate'] = account_data.get('tax_rate') doc = frappe.get_doc(template) + + # Data in country wise json is already pre validated, hence validations can be ignored + # Ingone validations to make doctypes faster doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) From 22683cf19bf5ac603915f57c0f378663c3f1c7c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 13:17:01 +0530 Subject: [PATCH 475/550] fix: Tests --- erpnext/setup/setup_wizard/operations/install_fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 7ae81d782a..652e992741 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -479,7 +479,7 @@ def update_stock_settings(): stock_settings.save() def create_bank_account(args): - if not args.bank_account: + if not args.get('bank_account'): return company_name = args.company_name From 0dfea4d13450298f1b3c8cc66129995849335b42 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 23 Jun 2021 15:37:17 +0530 Subject: [PATCH 476/550] fix: Test Cases --- erpnext/setup/setup_wizard/operations/install_fixtures.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 652e992741..4860431941 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -482,14 +482,14 @@ def create_bank_account(args): if not args.get('bank_account'): return - company_name = args.company_name + company_name = args.get('company_name') bank_account_group = frappe.db.get_value("Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name}) if bank_account_group: bank_account = frappe.get_doc({ "doctype": "Account", - 'account_name': args.bank_account, + 'account_name': args.get('bank_account'), 'parent_account': bank_account_group, 'is_group':0, 'company': company_name, @@ -498,10 +498,10 @@ def create_bank_account(args): try: doc = bank_account.insert() - frappe.db.set_value("Company", args.company_name, "default_bank_account", bank_account.name, update_modified=False) + frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.bank_account)) + frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account'))) except frappe.DuplicateEntryError: # bank account same as a CoA entry pass From 2908f2ee2080d75524e2d5db0519f28979cc4b6b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 26 Jun 2021 23:49:32 +0530 Subject: [PATCH 477/550] fix: Revert Changes --- erpnext/public/js/setup_wizard.js | 26 +++++++ .../setup_wizard/data/country_wise_tax.json | 72 ++++++++++++------- .../setup_wizard/operations/taxes_setup.py | 15 ++-- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index a3045724fe..6f5d67c746 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -139,10 +139,36 @@ erpnext.setup.slides_settings = [ }, validate: function () { + let me = this; + let exist; + if (!this.validate_fy_dates()) { return false; } + // Validate bank name + if(me.values.bank_account) { + frappe.call({ + async: false, + method: "erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts.validate_bank_account", + args: { + "coa": me.values.chart_of_accounts, + "bank_account": me.values.bank_account + }, + callback: function (r) { + if(r.message){ + exist = r.message; + me.get_field("bank_account").set_value(""); + let message = __('Account {0} already exists. Please enter a different name for your bank account.', + [me.values.bank_account] + ); + frappe.msgprint(message); + } + } + }); + return !exist; // Return False if exist = true + } + return true; }, diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index e36bf5cbe0..34af093a23 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1218,37 +1218,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 18.00 + "tax_rate": 18.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 9.00 + "tax_rate": 9.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 18.00 + "tax_rate": 18.00, + "root_type": "Asset" } } ] @@ -1277,37 +1283,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 2.5 + "tax_rate": 2.5, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 2.5 + "tax_rate": 2.5, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 5.0 + "tax_rate": 5.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 2.50 + "tax_rate": 2.50, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 2.50 + "tax_rate": 2.50, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 5.00 + "tax_rate": 5.00, + "root_type": "Asset" } } ] @@ -1336,37 +1348,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 6.0 + "tax_rate": 6.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 6.0 + "tax_rate": 6.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 12.0 + "tax_rate": 12.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 6.00 + "tax_rate": 6.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 6.00 + "tax_rate": 6.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 12.00 + "tax_rate": 12.00, + "root_type": "Asset" } } ] @@ -1395,37 +1413,43 @@ { "tax_type": { "account_name": "Input Tax SGST", - "tax_rate": 14.0 + "tax_rate": 14.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST", - "tax_rate": 14.0 + "tax_rate": 14.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST", - "tax_rate": 28.0 + "tax_rate": 28.0, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax SGST RCM", - "tax_rate": 14.00 + "tax_rate": 14.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax CGST RCM", - "tax_rate": 14.00 + "tax_rate": 14.00, + "root_type": "Asset" } }, { "tax_type": { "account_name": "Input Tax IGST RCM", - "tax_rate": 28.00 + "tax_rate": 28.00, + "root_type": "Asset" } } ] diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index c42a0416a0..9b7e3d8fcf 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -202,16 +202,15 @@ def get_or_create_account(company_name, account): existing_accounts = frappe.get_list('Account', filters={ - 'account_name': account.get('account_name'), - 'account_number': account.get('account_number', ''), - 'company': company_name + 'company': company_name, + 'root_type': root_type }, or_filters={ - 'company': company_name, - 'root_type': root_type, - 'is_group': 0 - } - ) + 'account_name': account.get('account_name'), + 'account_number': account.get('account_number') + }) + + print(company_name, account, existing_accounts) if existing_accounts: return frappe.get_doc('Account', existing_accounts[0].name) From bb679cc03678a3d8c25bdc561b6ef738a5b5416d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 26 Jun 2021 23:59:47 +0530 Subject: [PATCH 478/550] fix: Add validate bank account method back --- .../chart_of_accounts/chart_of_accounts.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 9b6842d896..927adc7086 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -188,6 +188,24 @@ def build_account_tree(tree, parent, all_accounts): # call recursively to build a subtree for current account build_account_tree(tree[child.account_name], child, all_accounts) +@frappe.whitelist() +def validate_bank_account(coa, bank_account): + accounts = [] + chart = get_chart(coa) + + if chart: + def _get_account_names(account_master): + for account_name, child in iteritems(account_master): + if account_name not in ["account_number", "account_type", + "root_type", "is_group", "tax_rate"]: + accounts.append(account_name) + + _get_account_names(child) + + _get_account_names(chart) + + return (bank_account in accounts) + @frappe.whitelist() def build_tree_from_json(chart_template, chart_data=None): ''' get chart template from its folder and parse the json to be rendered as tree ''' From 779d2afa601f8ace9d2785ff3b00540082163129 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Jun 2021 10:52:38 +0530 Subject: [PATCH 479/550] fix: Update account heads in GST test cases --- .../sales_invoice/test_sales_invoice.py | 10 +++---- .../gstr_3b_report/test_gstr_3b_report.py | 28 +++++++++---------- .../setup_wizard/operations/taxes_setup.py | 2 -- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index fe531d3b22..b11e1b671f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2087,9 +2087,9 @@ def make_sales_invoice_for_ewaybill(): if not gst_account: gst_settings.append("gst_accounts", { "company": "_Test Company", - "cgst_account": "CGST - _TC", - "sgst_account": "SGST - _TC", - "igst_account": "IGST - _TC", + "cgst_account": "Output Tax CGST - _TC", + "sgst_account": "Output Tax SGST - _TC", + "igst_account": "Output Tax IGST - _TC", }) gst_settings.save() @@ -2106,7 +2106,7 @@ def make_sales_invoice_for_ewaybill(): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "CGST - _TC", + "account_head": "Output Tax CGST - _TC", "cost_center": "Main - _TC", "description": "CGST @ 9.0", "rate": 9 @@ -2114,7 +2114,7 @@ def make_sales_invoice_for_ewaybill(): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "SGST - _TC", + "account_head": "Output Tax SGST - _TC", "cost_center": "Main - _TC", "description": "SGST @ 9.0", "rate": 9 diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index 3857ce1cdb..065f80d610 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -46,14 +46,14 @@ class TestGSTR3BReport(unittest.TestCase): make_sales_invoice() create_purchase_invoices() - if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing"): - report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address-Billing") + if frappe.db.exists("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing"): + report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing") report.save() else: report = frappe.get_doc({ "doctype": "GSTR 3B Report", "company": "_Test Company GST", - "company_address": "_Test Address-Billing", + "company_address": "_Test Address GST-Billing", "year": getdate().year, "month": month_number_mapping.get(getdate().month) }).insert() @@ -89,7 +89,7 @@ class TestGSTR3BReport(unittest.TestCase): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -117,7 +117,7 @@ def make_sales_invoice(): si.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -138,7 +138,7 @@ def make_sales_invoice(): si1.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -159,7 +159,7 @@ def make_sales_invoice(): si2.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "Output Tax IGST - _GST", "cost_center": "Main - _GST", "description": "IGST @ 18.0", "rate": 18 @@ -195,7 +195,7 @@ def create_purchase_invoices(): pi.append("taxes", { "charge_type": "On Net Total", - "account_head": "CGST - _GST", + "account_head": "Input Tax CGST - _GST", "cost_center": "Main - _GST", "description": "CGST @ 9.0", "rate": 9 @@ -203,7 +203,7 @@ def create_purchase_invoices(): pi.append("taxes", { "charge_type": "On Net Total", - "account_head": "SGST - _GST", + "account_head": "Input Tax SGST - _GST", "cost_center": "Main - _GST", "description": "SGST @ 9.0", "rate": 9 @@ -410,10 +410,10 @@ def make_company(): company.country = "India" company.insert() - if not frappe.db.exists('Address', '_Test Address-Billing'): + if not frappe.db.exists('Address', '_Test Address GST-Billing'): address = frappe.get_doc({ + "address_title": "_Test Address GST", "address_line1": "_Test Address Line 1", - "address_title": "_Test Address", "address_type": "Billing", "city": "_Test City", "state": "Test State", @@ -444,9 +444,9 @@ def set_account_heads(): if not gst_account: gst_settings.append("gst_accounts", { "company": "_Test Company GST", - "cgst_account": "CGST - _GST", - "sgst_account": "SGST - _GST", - "igst_account": "IGST - _GST", + "cgst_account": "Output Tax CGST - _GST", + "sgst_account": "Output Tax SGST - _GST", + "igst_account": "Output Tax IGST - _GST" }) gst_settings.save() diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 9b7e3d8fcf..6ea0ca407c 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -210,8 +210,6 @@ def get_or_create_account(company_name, account): 'account_number': account.get('account_number') }) - print(company_name, account, existing_accounts) - if existing_accounts: return frappe.get_doc('Account', existing_accounts[0].name) From 8f6f86aff419ed9ce58e7c634e27e90c112aaaf7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 10:09:42 +0530 Subject: [PATCH 480/550] fix: Test cases for M-pesa --- .../doctype/mpesa_settings/test_mpesa_settings.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index 3c2e59ab82..2dfd5aad5d 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -9,13 +9,17 @@ from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import p from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice class TestMpesaSettings(unittest.TestCase): + def setUp(self): + # create payment gateway in setup + create_mpesa_settings(payment_gateway_name="_Test") + create_mpesa_settings(payment_gateway_name="_Account Balance") + create_mpesa_settings(payment_gateway_name="Payment") + def tearDown(self): frappe.db.sql('delete from `tabMpesa Settings`') frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"') def test_creation_of_payment_gateway(self): - create_mpesa_settings(payment_gateway_name="_Test") - mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test") self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"})) self.assertTrue(mode_of_payment.name) @@ -47,7 +51,6 @@ class TestMpesaSettings(unittest.TestCase): integration_request.delete() def test_processing_of_callback_payload(self): - create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES") @@ -90,7 +93,6 @@ class TestMpesaSettings(unittest.TestCase): pos_invoice.delete() def test_processing_of_multiple_callback_payload(self): - create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") @@ -141,7 +143,6 @@ class TestMpesaSettings(unittest.TestCase): pos_invoice.delete() def test_processing_of_only_one_succes_callback_payload(self): - create_mpesa_settings(payment_gateway_name="Payment") mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account") frappe.db.set_value("Account", mpesa_account, "account_currency", "KES") frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500") From b2af6b4583e977e34f8b88a56dde73cb83971711 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 13:46:03 +0530 Subject: [PATCH 481/550] fix: Create mode of payment if doesn't exists --- .../doctype/mpesa_settings/test_mpesa_settings.py | 3 ++- erpnext/erpnext_integrations/utils.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index 2dfd5aad5d..f592c180a3 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -7,6 +7,7 @@ import frappe import unittest from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice +from erpnext.erpnext_integrations.utils import create_mode_of_payment class TestMpesaSettings(unittest.TestCase): def setUp(self): @@ -20,7 +21,7 @@ class TestMpesaSettings(unittest.TestCase): frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"') def test_creation_of_payment_gateway(self): - mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test") + mode_of_payment = create_mode_of_payment('Mpesa-_Test', payment_type="Phone") self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"})) self.assertTrue(mode_of_payment.name) self.assertEqual(mode_of_payment.type, "Phone") diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index 3840e781b4..b764701103 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -52,7 +52,8 @@ def create_mode_of_payment(gateway, payment_type="General"): "payment_gateway": gateway }, ['payment_account']) - if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account: + mode_of_payment = frappe.db.exists("Mode of Payment", gateway) + if not mode_of_payment and payment_gateway_account: mode_of_payment = frappe.get_doc({ "doctype": "Mode of Payment", "mode_of_payment": gateway, @@ -66,6 +67,10 @@ def create_mode_of_payment(gateway, payment_type="General"): }) mode_of_payment.insert(ignore_permissions=True) + return mode_of_payment + else: + return frappe.get_doc("Mode of Payment", mode_of_payment) + def get_tracking_url(carrier, tracking_number): # Return the formatted Tracking URL. tracking_url = '' From 3ef394c556f3e672b301442e5930cc4ab7c002cc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 14:45:33 +0530 Subject: [PATCH 482/550] fix: Test cases --- erpnext/erpnext_integrations/utils.py | 2 +- erpnext/hr/doctype/expense_claim/test_expense_claim.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/utils.py b/erpnext/erpnext_integrations/utils.py index b764701103..a5e162f8b5 100644 --- a/erpnext/erpnext_integrations/utils.py +++ b/erpnext/erpnext_integrations/utils.py @@ -68,7 +68,7 @@ def create_mode_of_payment(gateway, payment_type="General"): mode_of_payment.insert(ignore_permissions=True) return mode_of_payment - else: + elif mode_of_payment: return frappe.get_doc("Mode of Payment", mode_of_payment) def get_tracking_url(carrier, tracking_number): diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 578eccf787..141561fcdc 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -72,7 +72,8 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_gl_entry(self): payable_account = get_payable_account(company_name) taxes = generate_taxes() - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", do_not_submit=True, taxes=taxes) + expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", + do_not_submit=True, taxes=taxes) expense_claim.submit() gl_entries = frappe.db.sql("""select account, debit, credit @@ -145,7 +146,7 @@ def generate_taxes(): parent_account = frappe.db.get_value('Account', {'company': company_name, 'is_group':1, 'account_type': 'Tax'}, 'name') - account = create_account(company=company_name, account_name="CGST", account_type="Tax", parent_account=parent_account) + account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account) return {'taxes':[{ "account_head": account, "rate": 0, From 2de11fbbc472adc2e7ac4ebcd9044db42302b2e8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 5 Jul 2021 17:08:27 +0530 Subject: [PATCH 483/550] fix: Test Cases --- .../doctype/mpesa_settings/test_mpesa_settings.py | 1 + erpnext/hr/doctype/expense_claim/test_expense_claim.py | 2 +- erpnext/setup/setup_wizard/operations/install_fixtures.py | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py index f592c180a3..b0e662d3f3 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py @@ -204,6 +204,7 @@ def create_mpesa_settings(payment_gateway_name="Express"): doc = frappe.get_doc(dict( #nosec doctype="Mpesa Settings", + sandbox=1, payment_gateway_name=payment_gateway_name, consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn", consumer_secret="VI1oS3oBGPJfh3JyvLHw", diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 141561fcdc..96ea686706 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -83,7 +83,7 @@ class TestExpenseClaim(unittest.TestCase): self.assertTrue(gl_entries) expected_values = dict((d[0], d) for d in [ - ['CGST - _TC4',18.0, 0.0], + ['Output Tax CGST - _TC4',18.0, 0.0], [payable_account, 0.0, 218.0], ["Travel Expenses - _TC4", 200.0, 0.0] ]) diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 4860431941..cd49a18052 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -448,6 +448,8 @@ def install_defaults(args=None): set_active_domains(args) update_stock_settings() update_shopping_cart_settings(args) + + args.update({"set_default": 1}) create_bank_account(args) def set_global_defaults(args): @@ -498,7 +500,10 @@ def create_bank_account(args): try: doc = bank_account.insert() - frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) + if args.get('set_default'): + frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) + + return doc except RootNotEditable: frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account'))) From 6357ffe4a142ff9ae861e26b9728b2cb110a0200 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 13 Jul 2021 22:31:01 +0530 Subject: [PATCH 484/550] fix: Create asset data --- .../doctype/sales_invoice/test_sales_invoice.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 31de48216b..c27a878dd8 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -10,7 +10,7 @@ from frappe.model.dynamic_links import get_dynamic_link_map from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile -from erpnext.assets.doctype.asset.test_asset import create_asset +from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from frappe.model.naming import make_autoname @@ -1071,11 +1071,11 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) def test_gle_made_when_asset_is_returned(self): - create_item(item_code="_Test Item linked with Asset", is_stock_item = 0, is_fixed_asset=1, asset_category="Computers") - asset = create_asset(item_code="_Test Item linked with Asset") + create_asset_data() + asset = create_asset(item_code="Macbook Pro") - si = create_sales_invoice(item_code="_Test Item linked with Asset", asset=asset.name, qty=1, rate=90000) - return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="_Test Item linked with Asset", asset=asset.name, qty=-1, rate=90000) + si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000) + return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000) disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account") From 59bf2df28e95259fd0046df24a2d33789b850f06 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 14 Jul 2021 11:40:58 +0530 Subject: [PATCH 485/550] fix: pos item cart dom updates (#26461) --- .../selling/page/point_of_sale/pos_item_cart.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 38508c219b..f7b2c1d93c 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -965,8 +965,23 @@ erpnext.PointOfSale.ItemCart = class { }); } + attach_refresh_field_event(frm) { + $(frm.wrapper).off('refresh-fields'); + $(frm.wrapper).on('refresh-fields', () => { + if (frm.doc.items.length) { + frm.doc.items.forEach(item => { + this.update_item_html(item); + }); + } + this.update_totals_section(frm); + }); + } + load_invoice() { const frm = this.events.get_frm(); + + this.attach_refresh_field_event(frm); + this.fetch_customer_details(frm.doc.customer).then(() => { this.events.customer_details_updated(this.customer_info); this.update_customer_section(); From f4fc1384a591e762ddfc64d32929f55402291952 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Wed, 14 Jul 2021 11:43:10 +0530 Subject: [PATCH 486/550] fix(Issue): Calculate first_response_time based on working hours (#25991) --- erpnext/hooks.py | 5 +- erpnext/support/doctype/issue/issue.py | 130 +++++++++- erpnext/support/doctype/issue/test_issue.py | 227 +++++++++++++++++- .../test_service_level_agreement.py | 10 - 4 files changed, 353 insertions(+), 19 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ba10b58f85..9717bb9b17 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -245,7 +245,10 @@ doc_events = { "erpnext.portal.utils.set_default_role"] }, "Communication": { - "on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time" + "on_update": [ + "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time", + "erpnext.support.doctype.issue.issue.set_first_response_time" + ] }, ("Sales Taxes and Charges Template", 'Price List'): { "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index dd6d647abc..b9a65b6749 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -5,10 +5,10 @@ from __future__ import unicode_literals import frappe import json from frappe import _ -from frappe import utils from frappe.model.document import Document -from frappe.utils import now_datetime -from datetime import datetime, timedelta +from frappe.utils import now_datetime, time_diff_in_seconds, get_datetime, date_diff +from frappe.core.utils import get_parent_doc +from datetime import timedelta from frappe.model.mapper import get_mapped_doc from frappe.utils.user import is_website_user from frappe.email.inbox import link_communication_to_document @@ -212,7 +212,129 @@ def make_issue_from_communication(communication, ignore_communication_links=Fals return issue.name +def get_time_in_timedelta(time): + """ + Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215) + """ + return timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) + +def set_first_response_time(communication, method): + if communication.get('reference_doctype') == "Issue": + issue = get_parent_doc(communication) + if is_first_response(issue): + first_response_time = calculate_first_response_time(issue, get_datetime(issue.first_responded_on)) + issue.db_set("first_response_time", first_response_time) + +def is_first_response(issue): + responses = frappe.get_all('Communication', filters = {'reference_name': issue.name, 'sent_or_received': 'Sent'}) + if len(responses) == 1: + return True + return False + +def calculate_first_response_time(issue, first_responded_on): + issue_creation_date = issue.creation + issue_creation_time = get_time_in_seconds(issue_creation_date) + first_responded_on_in_seconds = get_time_in_seconds(first_responded_on) + support_hours = frappe.get_cached_doc("Service Level Agreement", issue.service_level_agreement).support_and_resolution + + if issue_creation_date.day == first_responded_on.day: + if is_work_day(issue_creation_date, support_hours): + start_time, end_time = get_working_hours(issue_creation_date, support_hours) + + # issue creation and response on the same day during working hours + if is_during_working_hours(issue_creation_date, support_hours) and is_during_working_hours(first_responded_on, support_hours): + return get_elapsed_time(issue_creation_date, first_responded_on) + + # issue creation is during working hours, but first response was after working hours + elif is_during_working_hours(issue_creation_date, support_hours): + return get_elapsed_time(issue_creation_time, end_time) + + # issue creation was before working hours but first response is during working hours + elif is_during_working_hours(first_responded_on, support_hours): + return get_elapsed_time(start_time, first_responded_on_in_seconds) + + # both issue creation and first response were after working hours + else: + return 1.0 # this should ideally be zero, but it gets reset when the next response is sent if the value is zero + + else: + return 1.0 + + else: + # response on the next day + if date_diff(first_responded_on, issue_creation_date) == 1: + first_response_time = 0 + else: + first_response_time = calculate_initial_frt(issue_creation_date, date_diff(first_responded_on, issue_creation_date)- 1, support_hours) + + # time taken on day of issue creation + if is_work_day(issue_creation_date, support_hours): + start_time, end_time = get_working_hours(issue_creation_date, support_hours) + + if is_during_working_hours(issue_creation_date, support_hours): + first_response_time += get_elapsed_time(issue_creation_time, end_time) + elif is_before_working_hours(issue_creation_date, support_hours): + first_response_time += get_elapsed_time(start_time, end_time) + + # time taken on day of first response + if is_work_day(first_responded_on, support_hours): + start_time, end_time = get_working_hours(first_responded_on, support_hours) + + if is_during_working_hours(first_responded_on, support_hours): + first_response_time += get_elapsed_time(start_time, first_responded_on_in_seconds) + elif not is_before_working_hours(first_responded_on, support_hours): + first_response_time += get_elapsed_time(start_time, end_time) + + if first_response_time: + return first_response_time + else: + return 1.0 + +def get_time_in_seconds(date): + return timedelta(hours=date.hour, minutes=date.minute, seconds=date.second) + +def get_working_hours(date, support_hours): + if is_work_day(date, support_hours): + weekday = frappe.utils.get_weekday(date) + for day in support_hours: + if day.workday == weekday: + return day.start_time, day.end_time + +def is_work_day(date, support_hours): + weekday = frappe.utils.get_weekday(date) + for day in support_hours: + if day.workday == weekday: + return True + return False + +def is_during_working_hours(date, support_hours): + start_time, end_time = get_working_hours(date, support_hours) + time = get_time_in_seconds(date) + if time >= start_time and time <= end_time: + return True + return False + +def get_elapsed_time(start_time, end_time): + return round(time_diff_in_seconds(end_time, start_time), 2) + +def calculate_initial_frt(issue_creation_date, days_in_between, support_hours): + initial_frt = 0 + for i in range(days_in_between): + date = issue_creation_date + timedelta(days = (i+1)) + if is_work_day(date, support_hours): + start_time, end_time = get_working_hours(date, support_hours) + initial_frt += get_elapsed_time(start_time, end_time) + + return initial_frt + +def is_before_working_hours(date, support_hours): + start_time, end_time = get_working_hours(date, support_hours) + time = get_time_in_seconds(date) + if time < start_time: + return True + return False + def get_holidays(holiday_list_name): holiday_list = frappe.get_cached_doc("Holiday List", holiday_list_name) holidays = [holiday.holiday_date for holiday in holiday_list.holidays] - return holidays \ No newline at end of file + return holidays diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index 7b9b1446d4..84f8c398be 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -5,16 +5,18 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues -from frappe.utils import now_datetime, get_datetime, flt +from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.utils import get_datetime, flt import datetime from datetime import timedelta -class TestIssue(unittest.TestCase): +class TestSetUp(unittest.TestCase): def setUp(self): frappe.db.sql("delete from `tabService Level Agreement`") frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) create_service_level_agreements_for_issues() +class TestIssue(TestSetUp): def test_response_time_and_resolution_time_based_on_different_sla(self): creation = datetime.datetime(2019, 3, 4, 12, 0) @@ -133,6 +135,223 @@ class TestIssue(unittest.TestCase): issue.reload() self.assertEqual(flt(issue.total_hold_time, 2), 2700) +class TestFirstResponseTime(TestSetUp): + # working hours used in all cases: Mon-Fri, 10am to 6pm + # all dates are in the mm-dd-yyyy format + + # issue creation and first response are on the same day + def test_first_response_time_case1(self): + """ + Test frt when issue creation and first response are during working hours on the same day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 11:00"), get_datetime("06-28-2021 12:00")) + self.assertEqual(issue.first_response_time, 3600.0) + + def test_first_response_time_case2(self): + """ + Test frt when issue creation was during working hours, but first response is sent after working hours on the same day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-28-2021 20:00")) + self.assertEqual(issue.first_response_time, 21600.0) + + def test_first_response_time_case3(self): + """ + Test frt when issue creation was before working hours but first response is sent during working hours on the same day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-28-2021 12:00")) + self.assertEqual(issue.first_response_time, 7200.0) + + def test_first_response_time_case4(self): + """ + Test frt when both issue creation and first response were after working hours on the same day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 19:00"), get_datetime("06-28-2021 20:00")) + self.assertEqual(issue.first_response_time, 1.0) + + def test_first_response_time_case5(self): + """ + Test frt when both issue creation and first response are on the same day, but it's not a work day. + """ + issue = create_issue_and_communication(get_datetime("06-27-2021 10:00"), get_datetime("06-27-2021 11:00")) + self.assertEqual(issue.first_response_time, 1.0) + + # issue creation and first response are on consecutive days + def test_first_response_time_case6(self): + """ + Test frt when the issue was created before working hours and the first response is also sent before working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 6:00")) + self.assertEqual(issue.first_response_time, 28800.0) + + def test_first_response_time_case7(self): + """ + Test frt when the issue was created before working hours and the first response is sent during working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 11:00")) + self.assertEqual(issue.first_response_time, 32400.0) + + def test_first_response_time_case8(self): + """ + Test frt when the issue was created before working hours and the first response is sent after working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("06-29-2021 20:00")) + self.assertEqual(issue.first_response_time, 57600.0) + + def test_first_response_time_case9(self): + """ + Test frt when the issue was created before working hours and the first response is sent on the next day, which is not a work day. + """ + issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-26-2021 11:00")) + self.assertEqual(issue.first_response_time, 28800.0) + + def test_first_response_time_case10(self): + """ + Test frt when the issue was created during working hours and the first response is sent before working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 6:00")) + self.assertEqual(issue.first_response_time, 21600.0) + + def test_first_response_time_case11(self): + """ + Test frt when the issue was created during working hours and the first response is also sent during working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 11:00")) + self.assertEqual(issue.first_response_time, 25200.0) + + def test_first_response_time_case12(self): + """ + Test frt when the issue was created during working hours and the first response is sent after working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("06-29-2021 20:00")) + self.assertEqual(issue.first_response_time, 50400.0) + + def test_first_response_time_case13(self): + """ + Test frt when the issue was created during working hours and the first response is sent on the next day, which is not a work day. + """ + issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-26-2021 11:00")) + self.assertEqual(issue.first_response_time, 21600.0) + + def test_first_response_time_case14(self): + """ + Test frt when the issue was created after working hours and the first response is sent before working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 6:00")) + self.assertEqual(issue.first_response_time, 1.0) + + def test_first_response_time_case15(self): + """ + Test frt when the issue was created after working hours and the first response is sent during working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 11:00")) + self.assertEqual(issue.first_response_time, 3600.0) + + def test_first_response_time_case16(self): + """ + Test frt when the issue was created after working hours and the first response is also sent after working hours, but on the next day. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("06-29-2021 20:00")) + self.assertEqual(issue.first_response_time, 28800.0) + + def test_first_response_time_case17(self): + """ + Test frt when the issue was created after working hours and the first response is sent on the next day, which is not a work day. + """ + issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-26-2021 11:00")) + self.assertEqual(issue.first_response_time, 1.0) + + # issue creation and first response are a few days apart + def test_first_response_time_case18(self): + """ + Test frt when the issue was created before working hours and the first response is also sent before working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 6:00")) + self.assertEqual(issue.first_response_time, 86400.0) + + def test_first_response_time_case19(self): + """ + Test frt when the issue was created before working hours and the first response is sent during working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 11:00")) + self.assertEqual(issue.first_response_time, 90000.0) + + def test_first_response_time_case20(self): + """ + Test frt when the issue was created before working hours and the first response is sent after working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 6:00"), get_datetime("07-01-2021 20:00")) + self.assertEqual(issue.first_response_time, 115200.0) + + def test_first_response_time_case21(self): + """ + Test frt when the issue was created before working hours and the first response is sent after a few days, on a holiday. + """ + issue = create_issue_and_communication(get_datetime("06-25-2021 6:00"), get_datetime("06-27-2021 11:00")) + self.assertEqual(issue.first_response_time, 28800.0) + + def test_first_response_time_case22(self): + """ + Test frt when the issue was created during working hours and the first response is sent before working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 6:00")) + self.assertEqual(issue.first_response_time, 79200.0) + + def test_first_response_time_case23(self): + """ + Test frt when the issue was created during working hours and the first response is also sent during working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 11:00")) + self.assertEqual(issue.first_response_time, 82800.0) + + def test_first_response_time_case24(self): + """ + Test frt when the issue was created during working hours and the first response is sent after working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 12:00"), get_datetime("07-01-2021 20:00")) + self.assertEqual(issue.first_response_time, 108000.0) + + def test_first_response_time_case25(self): + """ + Test frt when the issue was created during working hours and the first response is sent after a few days, on a holiday. + """ + issue = create_issue_and_communication(get_datetime("06-25-2021 12:00"), get_datetime("06-27-2021 11:00")) + self.assertEqual(issue.first_response_time, 21600.0) + + def test_first_response_time_case26(self): + """ + Test frt when the issue was created after working hours and the first response is sent before working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 6:00")) + self.assertEqual(issue.first_response_time, 57600.0) + + def test_first_response_time_case27(self): + """ + Test frt when the issue was created after working hours and the first response is sent during working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 11:00")) + self.assertEqual(issue.first_response_time, 61200.0) + + def test_first_response_time_case28(self): + """ + Test frt when the issue was created after working hours and the first response is also sent after working hours, but after a few days. + """ + issue = create_issue_and_communication(get_datetime("06-28-2021 20:00"), get_datetime("07-01-2021 20:00")) + self.assertEqual(issue.first_response_time, 86400.0) + + def test_first_response_time_case29(self): + """ + Test frt when the issue was created after working hours and the first response is sent after a few days, on a holiday. + """ + issue = create_issue_and_communication(get_datetime("06-25-2021 20:00"), get_datetime("06-27-2021 11:00")) + self.assertEqual(issue.first_response_time, 1.0) + +def create_issue_and_communication(issue_creation, first_responded_on): + issue = make_issue(issue_creation, index=1) + sender = create_user("test@admin.com") + create_communication(issue.name, sender.email, "Sent", first_responded_on) + issue.reload() + + return issue def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None): issue = frappe.get_doc({ @@ -185,7 +404,7 @@ def create_territory(territory): def create_communication(reference_name, sender, sent_or_received, creation): - issue = frappe.get_doc({ + communication = frappe.get_doc({ "doctype": "Communication", "communication_type": "Communication", "communication_medium": "Email", @@ -199,4 +418,4 @@ def create_communication(reference_name, sender, sent_or_received, creation): "creation": creation, "reference_name": reference_name }) - issue.save() + communication.save() \ No newline at end of file diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 865fadc97c..7bc97d6022 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -339,16 +339,6 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list "workday": "Friday", "start_time": "10:00:00", "end_time": "18:00:00", - }, - { - "workday": "Saturday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Sunday", - "start_time": "10:00:00", - "end_time": "18:00:00", } ] }) From 05d7c69aa2b64aa130a994e3b6d447967db442d0 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Wed, 14 Jul 2021 11:54:27 +0530 Subject: [PATCH 487/550] fix: delete child docs when parent doc is deleted (#26239) --- .../transaction_deletion_record.py | 186 +++++++++++------- 1 file changed, 112 insertions(+), 74 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index ece9fb5699..691d331c74 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -12,10 +12,14 @@ from frappe.desk.notifications import clear_notifications class TransactionDeletionRecord(Document): def validate(self): frappe.only_for('System Manager') + self.validate_doctypes_to_be_ignored() + + def validate_doctypes_to_be_ignored(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in self.doctypes_to_be_ignored: if doctype.doctype_name not in doctypes_to_be_ignored_list: - frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), title=_("Not Allowed")) + frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), + title=_("Not Allowed")) def before_submit(self): if not self.doctypes_to_be_ignored: @@ -23,54 +27,9 @@ class TransactionDeletionRecord(Document): self.delete_bins() self.delete_lead_addresses() - - company_obj = frappe.get_doc('Company', self.company) - # reset company values - company_obj.total_monthly_sales = 0 - company_obj.sales_monthly_history = None - company_obj.save() - # Clear notification counts + self.reset_company_values() clear_notifications() - - singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') - tables = frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name') - doctypes_to_be_ignored_list = singles - for doctype in self.doctypes_to_be_ignored: - doctypes_to_be_ignored_list.append(doctype.doctype_name) - - docfields = frappe.get_all('DocField', - filters = { - 'fieldtype': 'Link', - 'options': 'Company', - 'parent': ['not in', doctypes_to_be_ignored_list]}, - fields=['parent', 'fieldname']) - - for docfield in docfields: - if docfield['parent'] != self.doctype: - no_of_docs = frappe.db.count(docfield['parent'], { - docfield['fieldname'] : self.company - }) - - if no_of_docs > 0: - self.delete_version_log(docfield['parent'], docfield['fieldname']) - self.delete_communications(docfield['parent'], docfield['fieldname']) - - # populate DocTypes table - if docfield['parent'] not in tables: - self.append('doctypes', { - 'doctype_name' : docfield['parent'], - 'no_of_docs' : no_of_docs - }) - - # delete the docs linked with the specified company - frappe.db.delete(docfield['parent'], { - docfield['fieldname'] : self.company - }) - - naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') - if naming_series: - if '#' in naming_series: - self.update_naming_series(naming_series, docfield['parent']) + self.delete_company_transactions() def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() @@ -79,6 +38,111 @@ class TransactionDeletionRecord(Document): 'doctype_name' : doctype }) + def delete_bins(self): + frappe.db.sql("""delete from tabBin where warehouse in + (select name from tabWarehouse where company=%s)""", self.company) + + def delete_lead_addresses(self): + """Delete addresses to which leads are linked""" + leads = frappe.get_all('Lead', filters={'company': self.company}) + leads = ["'%s'" % row.get("name") for row in leads] + addresses = [] + if leads: + addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name + in ({leads})""".format(leads=",".join(leads))) + + if addresses: + addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] + + frappe.db.sql("""delete from tabAddress where name in ({addresses}) and + name not in (select distinct dl1.parent from `tabDynamic Link` dl1 + inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent + and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))) + + frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead' + and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))) + + frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) + + def reset_company_values(self): + company_obj = frappe.get_doc('Company', self.company) + company_obj.total_monthly_sales = 0 + company_obj.sales_monthly_history = None + company_obj.save() + + def delete_company_transactions(self): + doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() + docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) + + tables = self.get_all_child_doctypes() + for docfield in docfields: + if docfield['parent'] != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + + if no_of_docs > 0: + self.delete_version_log(docfield['parent'], docfield['fieldname']) + self.delete_communications(docfield['parent'], docfield['fieldname']) + self.populate_doctypes_table(tables, docfield['parent'], no_of_docs) + + self.delete_child_tables(docfield['parent'], docfield['fieldname']) + self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + + naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') + if naming_series: + if '#' in naming_series: + self.update_naming_series(naming_series, docfield['parent']) + + def get_doctypes_to_be_ignored_list(self): + singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') + doctypes_to_be_ignored_list = singles + for doctype in self.doctypes_to_be_ignored: + doctypes_to_be_ignored_list.append(doctype.doctype_name) + + return doctypes_to_be_ignored_list + + def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list): + docfields = frappe.get_all('DocField', + filters = { + 'fieldtype': 'Link', + 'options': 'Company', + 'parent': ['not in', doctypes_to_be_ignored_list]}, + fields=['parent', 'fieldname']) + + return docfields + + def get_all_child_doctypes(self): + return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name') + + def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname): + return frappe.db.count(doctype, {company_fieldname : self.company}) + + def populate_doctypes_table(self, tables, doctype, no_of_docs): + if doctype not in tables: + self.append('doctypes', { + 'doctype_name' : doctype, + 'no_of_docs' : no_of_docs + }) + + def delete_child_tables(self, doctype, company_fieldname): + parent_docs_to_be_deleted = frappe.get_all(doctype, { + company_fieldname : self.company + }, pluck = 'name') + + child_tables = frappe.get_all('DocField', filters = { + 'fieldtype': 'Table', + 'parent': doctype + }, pluck = 'options') + + for table in child_tables: + frappe.db.delete(table, { + 'parent': ['in', parent_docs_to_be_deleted] + }) + + def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): + frappe.db.delete(doctype, { + company_fieldname : self.company + }) + def update_naming_series(self, naming_series, doctype_name): if '.' in naming_series: prefix, hashes = naming_series.rsplit('.', 1) @@ -107,32 +171,6 @@ class TransactionDeletionRecord(Document): frappe.delete_doc('Communication', communication_names, ignore_permissions=True) - def delete_bins(self): - frappe.db.sql("""delete from tabBin where warehouse in - (select name from tabWarehouse where company=%s)""", self.company) - - def delete_lead_addresses(self): - """Delete addresses to which leads are linked""" - leads = frappe.get_all('Lead', filters={'company': self.company}) - leads = ["'%s'" % row.get("name") for row in leads] - addresses = [] - if leads: - addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name - in ({leads})""".format(leads=",".join(leads))) - - if addresses: - addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] - - frappe.db.sql("""delete from tabAddress where name in ({addresses}) and - name not in (select distinct dl1.parent from `tabDynamic Link` dl1 - inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent - and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))) - - frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead' - and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))) - - frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) - @frappe.whitelist() def get_doctypes_to_be_ignored(): doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget', From c978fdf3dff0d21ee21097767c014a19aa85eef9 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 14 Jul 2021 13:53:30 +0530 Subject: [PATCH 488/550] fix: test fails due to improper gain loss account set (#26482) --- .../purchase_invoice/test_purchase_invoice.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index a04d082f19..db6f143eb8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -975,8 +975,17 @@ class TestPurchaseInvoice(unittest.TestCase): acc_settings.save() def test_gain_loss_with_advance_entry(self): - unlink_enabled = frappe.db.get_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice") - frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) + unlink_enabled = frappe.db.get_value( + "Accounts Settings", "Accounts Settings", + "unlink_payment_on_cancel_of_invoice") + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", + "unlink_payment_on_cancel_of_invoice", 1) + + original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") + frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC") + pay = frappe.get_doc({ 'doctype': 'Payment Entry', 'company': '_Test Company', @@ -1016,7 +1025,8 @@ class TestPurchaseInvoice(unittest.TestCase): gl_entries = frappe.db.sql(""" select account, sum(debit - credit) as balance from `tabGL Entry` where voucher_no=%s - group by account order by account asc""", (pi.name), as_dict=1) + group by account + order by account asc""", (pi.name), as_dict=1) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1076,6 +1086,7 @@ class TestPurchaseInvoice(unittest.TestCase): pay.cancel() frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled) + frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) def test_purchase_invoice_advance_taxes(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order From 1c76154096b4b7628725507399f42980ef6cb788 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 14 Jul 2021 14:45:39 +0530 Subject: [PATCH 489/550] fix: tds computation summary shows cancelled invoices (#26456) --- .../report/tds_computation_summary/tds_computation_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index e15715dccd..6b9df41f54 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f select voucher_no, credit from `tabGL Entry` where party in (%s) and credit > 0 - and company=%s and posting_date between %s and %s + and company=%s and is_cancelled = 0 + and posting_date between %s and %s """, (supplier, company, from_date, to_date), as_dict=1) supplier_credit_amount = flt(sum(d.credit for d in entries)) From ccb52f19bce478b4271bc5724bdc7d160b6c20f9 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 14 Jul 2021 14:46:16 +0530 Subject: [PATCH 490/550] fix: task status loop (#26006) Co-authored-by: Rucha Mahabal --- erpnext/projects/doctype/task/task.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 39a6024e2c..5976e016fa 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -77,9 +77,6 @@ class Task(NestedSet): if flt(self.progress or 0) > 100: frappe.throw(_("Progress % for a task cannot be more than 100.")) - if flt(self.progress) == 100: - self.status = 'Completed' - if self.status == 'Completed': self.progress = 100 From ddbf7c0020cce70d66cf7f7b6807546e7ce1e3ec Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 13 Jul 2021 17:27:55 +0530 Subject: [PATCH 491/550] fix: set item group as a persistent filter --- erpnext/portal/product_configurator/utils.py | 6 ++++++ erpnext/templates/generators/item_group.html | 2 +- erpnext/www/all-products/index.js | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index d77eb2c396..211b94a9cf 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -2,6 +2,7 @@ import frappe from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.shopping_cart.product_info import get_product_info_for_website +from erpnext.setup.doctype.item_group.item_group import get_child_groups def get_field_filter_data(): product_settings = get_product_settings() @@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search= def get_products_html_for_website(field_filters=None, attribute_filters=None): field_filters = frappe.parse_json(field_filters) attribute_filters = frappe.parse_json(attribute_filters) + set_item_group_filters(field_filters) items = get_products_for_website(field_filters, attribute_filters) html = ''.join(get_html_for_items(items)) @@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None): return html +def set_item_group_filters(field_filters): + if 'item_group' in field_filters: + field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])] + def get_item_codes_by_attributes(attribute_filters, template_item_code=None): items = [] diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 393c3a43af..95eb8f493f 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -9,7 +9,7 @@ {% endblock %} {% block page_content %} -
    +
    {% if slideshow %} {{ web_block( diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 0721056816..1c641b59ad 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -124,6 +124,10 @@ $(() => { attribute_filters: if_key_exists(attribute_filters) }; + const item_group = $(".item-group-content").data('item-group'); + if (item_group) { + Object.assign(field_filters, { item_group }); + } return new Promise((resolve, reject) => { frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args) .then(r => { From 72715956f125c45ff3916506a0505f08d4645812 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 14 Jul 2021 15:56:20 +0530 Subject: [PATCH 492/550] fix: tds computation summary shows cancelled invoices (#26486) --- .../report/tds_computation_summary/tds_computation_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index e15715dccd..6b9df41f54 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f select voucher_no, credit from `tabGL Entry` where party in (%s) and credit > 0 - and company=%s and posting_date between %s and %s + and company=%s and is_cancelled = 0 + and posting_date between %s and %s """, (supplier, company, from_date, to_date), as_dict=1) supplier_credit_amount = flt(sum(d.credit for d in entries)) From 24e08301bc41fd745eba2f4ff2181c72ddbde475 Mon Sep 17 00:00:00 2001 From: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com> Date: Wed, 14 Jul 2021 16:02:49 +0530 Subject: [PATCH 493/550] fix: update integration links in help.js (#26483) --- erpnext/public/js/help_links.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index 5c9a453e7d..d0c935f488 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -54,7 +54,7 @@ frappe.help.help_links["permission-manager"] = [ frappe.help.help_links["Form/System Settings"] = [ { - label: "Naming Series", + label: "System Settings", url: docsUrl + "user/manual/en/setting-up/settings/system-settings", }, ]; @@ -206,7 +206,7 @@ frappe.help.help_links["Form/PayPal Settings"] = [ label: "PayPal Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/paypal-integration", + "user/manual/en/erpnext_integration/paypal-integration", }, ]; @@ -215,14 +215,14 @@ frappe.help.help_links["Form/Razorpay Settings"] = [ label: "Razorpay Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/razorpay-integration", + "user/manual/en/erpnext_integration/razorpay-integration", }, ]; frappe.help.help_links["Form/Dropbox Settings"] = [ { label: "Dropbox Settings", - url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup", + url: docsUrl + "user/manual/en/erpnext_integration/dropbox-backup", }, ]; @@ -230,7 +230,7 @@ frappe.help.help_links["Form/LDAP Settings"] = [ { label: "LDAP Settings", url: - docsUrl + "user/manual/en/setting-up/integrations/ldap-integration", + docsUrl + "user/manual/en/erpnext_integration/ldap-integration", }, ]; @@ -239,7 +239,7 @@ frappe.help.help_links["Form/Stripe Settings"] = [ label: "Stripe Settings", url: docsUrl + - "user/manual/en/setting-up/integrations/stripe-integration", + "user/manual/en/erpnext_integration/stripe-integration", }, ]; From c932baeb321ddabba6b69a77d95ef6e5b9e04183 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 14 Jul 2021 16:25:42 +0530 Subject: [PATCH 494/550] fix: validation check for batch for stock reconciliation type in stock entry (#26370) * fix(ux): added filter for valid batch nos. * fix: not validating batch no if entry type stock reconciliation * test: validate batch_no --- .../stock_ledger_entry/stock_ledger_entry.py | 19 ++++++++--------- .../stock_reconciliation.js | 8 +++++++ .../test_stock_reconciliation.py | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index cb939e63c2..93482e8bea 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -89,17 +89,16 @@ class StockLedgerEntry(Document): if item_det.is_stock_item != 1: frappe.throw(_("Item {0} must be a stock Item").format(self.item_code)) - # check if batch number is required - if self.voucher_type != 'Stock Reconciliation': - if item_det.has_batch_no == 1: - batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name - if not self.batch_no: - frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) - elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): - frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) + # check if batch number is valid + if item_det.has_batch_no == 1: + batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name + if not self.batch_no: + frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) + elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): + frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) - elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: - frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) + elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: + frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 76a3f1a68d..4540954489 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -17,6 +17,14 @@ frappe.ui.form.on("Stock Reconciliation", { } } }); + frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + filters: { + 'item': item.item_code + } + }; + }); if (frm.doc.company) { erpnext.queries.setup_queries(frm, "Warehouse", function() { diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 7b98c7b3e2..cbe413b753 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -16,6 +16,7 @@ from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valua from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + class TestStockReconciliation(unittest.TestCase): @classmethod def setUpClass(self): @@ -316,6 +317,26 @@ class TestStockReconciliation(unittest.TestCase): dn2.cancel() pr1.cancel() + def test_valid_batch(self): + create_batch_item_with_batch("Testing Batch Item 1", "001") + create_batch_item_with_batch("Testing Batch Item 2", "002") + sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" + , do_not_submit=True) + self.assertRaises(frappe.ValidationError, sr.submit) + +def create_batch_item_with_batch(item_name, batch_id): + batch_item_doc = create_item(item_name, is_stock_item=1) + if not batch_item_doc.has_batch_no: + batch_item_doc.has_batch_no = 1 + batch_item_doc.create_new_batch = 1 + batch_item_doc.save(ignore_permissions=True) + + if not frappe.db.exists('Batch', batch_id): + b = frappe.new_doc('Batch') + b.item = item_name + b.batch_id = batch_id + b.save() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From 03f4db0606cc307c47a253eb4dc91fa230f96d93 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 14 Jul 2021 16:28:54 +0530 Subject: [PATCH 495/550] fix: validation check for batch for stock reconciliation type in stock entry(bp #26370 ) (#26488) * fix(ux): added filter for valid batch nos. * fix: not validating batch no if entry type stock reconciliation * test: validate batch_no --- .../stock_ledger_entry/stock_ledger_entry.py | 19 ++++++++--------- .../stock_reconciliation.js | 8 +++++++ .../test_stock_reconciliation.py | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index cb939e63c2..93482e8bea 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -89,17 +89,16 @@ class StockLedgerEntry(Document): if item_det.is_stock_item != 1: frappe.throw(_("Item {0} must be a stock Item").format(self.item_code)) - # check if batch number is required - if self.voucher_type != 'Stock Reconciliation': - if item_det.has_batch_no == 1: - batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name - if not self.batch_no: - frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) - elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): - frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) + # check if batch number is valid + if item_det.has_batch_no == 1: + batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name + if not self.batch_no: + frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) + elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): + frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) - elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: - frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) + elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: + frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index a01db80da4..349e59f31d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -17,6 +17,14 @@ frappe.ui.form.on("Stock Reconciliation", { } } }); + frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + filters: { + 'item': item.item_code + } + }; + }); if (frm.doc.company) { erpnext.queries.setup_queries(frm, "Warehouse", function() { diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 84cdc49128..c192582531 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -16,6 +16,7 @@ from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valua from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + class TestStockReconciliation(unittest.TestCase): @classmethod def setUpClass(self): @@ -352,6 +353,26 @@ class TestStockReconciliation(unittest.TestCase): dn2.cancel() pr1.cancel() + def test_valid_batch(self): + create_batch_item_with_batch("Testing Batch Item 1", "001") + create_batch_item_with_batch("Testing Batch Item 2", "002") + sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" + , do_not_submit=True) + self.assertRaises(frappe.ValidationError, sr.submit) + +def create_batch_item_with_batch(item_name, batch_id): + batch_item_doc = create_item(item_name, is_stock_item=1) + if not batch_item_doc.has_batch_no: + batch_item_doc.has_batch_no = 1 + batch_item_doc.create_new_batch = 1 + batch_item_doc.save(ignore_permissions=True) + + if not frappe.db.exists('Batch', batch_id): + b = frappe.new_doc('Batch') + b.item = item_name + b.batch_id = batch_id + b.save() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From b24c0bfbf933799811cfdc31c0658506261af76d Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 13 Jul 2021 15:34:25 +0530 Subject: [PATCH 496/550] fix: show child item group items on portal --- erpnext/setup/doctype/item_group/item_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 1c72cebfa9..5fcad00af1 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -87,8 +87,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): if not field_filters: field_filters = {} - # Ensure the query remains within current item group - field_filters['item_group'] = self.name + # Ensure the query remains within current item group & sub group + field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)] engine = ProductQuery() context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) From 9ea5072c527b2aed4206c0e747ec5f0474992165 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 13 Jul 2021 17:27:55 +0530 Subject: [PATCH 497/550] fix: set item group as a persistent filter --- erpnext/portal/product_configurator/utils.py | 6 ++++++ erpnext/templates/generators/item_group.html | 2 +- erpnext/www/all-products/index.js | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index d77eb2c396..211b94a9cf 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -2,6 +2,7 @@ import frappe from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.shopping_cart.product_info import get_product_info_for_website +from erpnext.setup.doctype.item_group.item_group import get_child_groups def get_field_filter_data(): product_settings = get_product_settings() @@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search= def get_products_html_for_website(field_filters=None, attribute_filters=None): field_filters = frappe.parse_json(field_filters) attribute_filters = frappe.parse_json(attribute_filters) + set_item_group_filters(field_filters) items = get_products_for_website(field_filters, attribute_filters) html = ''.join(get_html_for_items(items)) @@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None): return html +def set_item_group_filters(field_filters): + if 'item_group' in field_filters: + field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])] + def get_item_codes_by_attributes(attribute_filters, template_item_code=None): items = [] diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 393c3a43af..95eb8f493f 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -9,7 +9,7 @@ {% endblock %} {% block page_content %} -
    +
    {% if slideshow %} {{ web_block( diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 0721056816..1c641b59ad 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -124,6 +124,10 @@ $(() => { attribute_filters: if_key_exists(attribute_filters) }; + const item_group = $(".item-group-content").data('item-group'); + if (item_group) { + Object.assign(field_filters, { item_group }); + } return new Promise((resolve, reject) => { frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args) .then(r => { From 74d7baa80a82d2db8a87eb09f389a084fdadee1a Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 14 Jul 2021 19:59:11 +0530 Subject: [PATCH 498/550] fix: filter by accounts with group by accounts (#26438) * fix: filter by accounts with group by accounts * fix: parsing json Co-authored-by: Saqib --- erpnext/accounts/report/general_ledger/general_ledger.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index e724e9b51b..1759fa3a48 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -55,9 +55,11 @@ def validate_filters(filters, account_details): if not account_details.get(account): frappe.throw(_("Account {0} does not exists").format(account)) - if (filters.get("account") and filters.get("group_by") == _('Group by Account') - and account_details[filters.account].is_group == 0): - frappe.throw(_("Can not filter based on Account, if grouped by Account")) + if (filters.get("account") and filters.get("group_by") == _('Group by Account')): + filters.account = frappe.parse_json(filters.get('account')) + for account in filters.account: + if account_details[account].is_group == 0: + frappe.throw(_("Can not filter based on Child Account, if grouped by Account")) if (filters.get("voucher_no") and filters.get("group_by") in [_('Group by Voucher')]): From 4c6e9529029067045954841c592f6ee62388c906 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 14 Jul 2021 20:01:36 +0530 Subject: [PATCH 499/550] fix: Paging buttons not working on item group portal page --- erpnext/templates/generators/item_group.html | 29 +++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 393c3a43af..12078d2379 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -127,15 +127,36 @@
    -
    -
    +
    +
    +
    +
    {% if frappe.form_dict.start|int > 0 %} - + {% endif %} {% if items|length >= page_length %} - + {% endif %}
    + + {% endblock %} \ No newline at end of file From f40da4ac4c7d011eefef9b7189330217078c9b98 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 14 Jul 2021 20:01:36 +0530 Subject: [PATCH 500/550] fix: Paging buttons not working on item group portal page --- erpnext/templates/generators/item_group.html | 29 +++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 95eb8f493f..9050cc388a 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -127,15 +127,36 @@
    -
    -
    +
    +
    +
    +
    {% if frappe.form_dict.start|int > 0 %} - + {% endif %} {% if items|length >= page_length %} - + {% endif %}
    + + {% endblock %} \ No newline at end of file From ca1169eeba6d61ffad707bd6f3b01e4d6a277f75 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 15 Jul 2021 12:03:41 +0530 Subject: [PATCH 501/550] fix: check if field_filters is None --- erpnext/portal/product_configurator/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 211b94a9cf..d60b1a2b05 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -101,7 +101,7 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None): return html def set_item_group_filters(field_filters): - if 'item_group' in field_filters: + if field_filters is not None and 'item_group' in field_filters: field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])] From eeecb25a020fb4cf76d5643951c3809f7dd01570 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 15 Jul 2021 15:31:42 +0530 Subject: [PATCH 502/550] fix: WIP needs to be set before submit on skip_transfer (#26499) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 180815d80e..a55b0b3df6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -239,7 +239,7 @@ class WorkOrder(Document): self.create_serial_no_batch_no() def on_submit(self): - if not self.wip_warehouse: + if not self.wip_warehouse and not self.skip_transfer: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) From 9997cce1eaa6e17ce0738a1adf78e99e07b817da Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Jul 2021 14:08:58 +0530 Subject: [PATCH 503/550] fix: FG item not fetched in manufacture entry --- .../doctype/work_order/test_work_order.py | 54 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 22 +++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 68de0b29d3..bf1ccb7159 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -513,6 +513,60 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_batch_size_for_fg_item(self): + fg_item = "Test Batch Size Item For BOM 3" + rm1 = "Test Batch Size Item RM 1 For BOM 3" + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]: + item_args = { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + } + + if item == fg_item: + item_args['has_batch_no'] = 1 + item_args['create_new_batch'] = 1 + item_args['batch_number_series'] = 'TBSI3.#####' + + make_item(item, item_args) + + bom_name = frappe.db.get_value("BOM", + {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + + if not bom_name: + bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom.save() + bom.submit() + bom_name = bom.name + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), + qty=30, do_not_save = True) + work_order.batch_size = 10 + work_order.insert() + work_order.submit() + self.assertEqual(work_order.has_batch_no, 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + self.assertEqual(row.qty, 10) + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + def test_partial_material_consumption(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 90b81ddb1d..c9838d75f1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1090,13 +1090,13 @@ class StockEntry(StockController): "is_finished_item": 1 } - if self.work_order and self.pro_doc.has_batch_no: + if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', + 'make_serial_no_batch_from_work_order', cache=True)): self.set_batchwise_finished_goods(args, item) else: - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) def set_batchwise_finished_goods(self, args, item): - qty = flt(self.fg_completed_qty) filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, @@ -1105,7 +1105,17 @@ class StockEntry(StockController): fields = ["qty_to_produce as qty", "produced_qty", "name"] - for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): + data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + + if not data: + self.add_finished_goods(args, item) + else: + self.add_batchwise_finished_good(data, args, item) + + def add_batchwise_finished_good(self, data, args, item): + qty = flt(self.fg_completed_qty) + + for row in data: batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue @@ -1121,9 +1131,9 @@ class StockEntry(StockController): args["qty"] = fg_qty args["batch_no"] = row.name - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) - def add_finisged_goods(self, args, item): + def add_finished_goods(self, args, item): self.add_to_stock_entry_detail({ item.name: args }, bom_no = self.bom_no) From 2f775ee53cb115511cd3be5d9d09f505bf46e656 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 15 Jul 2021 16:29:28 +0530 Subject: [PATCH 504/550] fix: WIP needs to be set before submit on skip_transfer (#26507) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 779ae42d65..0a8e5329c1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -239,7 +239,7 @@ class WorkOrder(Document): self.create_serial_no_batch_no() def on_submit(self): - if not self.wip_warehouse: + if not self.wip_warehouse and not self.skip_transfer: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) From 6442b5df11cf4f0b9f3c6c7efa245ec13d751531 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 15 Jul 2021 16:47:50 +0530 Subject: [PATCH 505/550] fix: set default operation time to 0 (#26510) --- erpnext/manufacturing/doctype/sub_operation/sub_operation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json index f63d2b9864..10cee32398 100644 --- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json @@ -19,6 +19,7 @@ "options": "Operation" }, { + "default": "0", "description": "Time in mins", "fieldname": "time_in_mins", "fieldtype": "Float", @@ -38,7 +39,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-12-07 18:09:18.005578", + "modified": "2021-07-15 16:39:41.635362", "modified_by": "Administrator", "module": "Manufacturing", "name": "Sub Operation", From c66277e06b807dc1741419073df8d049955841ff Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Thu, 15 Jul 2021 18:10:17 +0530 Subject: [PATCH 506/550] fix: improving ux for additional discount field (#26495) --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index f7b2c1d93c..6e36d2809a 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -367,15 +367,16 @@ erpnext.PointOfSale.ItemCart = class { `
    ` ); const me = this; + const frm = me.events.get_frm(); + let discount = frm.doc.additional_discount_percentage; this.discount_field = frappe.ui.form.make_control({ df: { label: __('Discount'), fieldtype: 'Data', - placeholder: __('Enter discount percentage.'), + placeholder: ( discount ? discount + '%' : __('Enter discount percentage.') ), input_class: 'input-xs', onchange: function() { - const frm = me.events.get_frm(); if (flt(this.value) != 0) { frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value)); me.hide_discount_control(this.value); From d8668f78f41f305cb5d34d39057de034569dda38 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 15 Jul 2021 18:32:15 +0530 Subject: [PATCH 507/550] fix: validation check when no conversion_factor (#26219) --- erpnext/stock/utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8a6a3a3e4a..b57b2aa6b8 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -314,13 +314,16 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto for row_idx, row in enumerate(result): data = row.items() if is_dict_obj else enumerate(row) for key, value in data: - if key not in convertible_columns or not conversion_factors[row_idx-1]: + if key not in convertible_columns: continue + # If no conversion factor for the UOM, defaults to 1 + if not conversion_factors[row_idx]: + conversion_factors[row_idx] = 1 if convertible_columns.get(key) == 'rate': - new_value = flt(value) * conversion_factors[row_idx-1] + new_value = flt(value) * conversion_factors[row_idx] else: - new_value = flt(value) / conversion_factors[row_idx-1] + new_value = flt(value) / conversion_factors[row_idx] if not is_dict_obj: row.insert(key+1, new_value) @@ -386,4 +389,4 @@ def is_reposting_item_valuation_in_progress(): reposting_in_progress = frappe.db.exists("Repost Item Valuation", {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) if reposting_in_progress: - frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) \ No newline at end of file + frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1) From a748dd926171e5a3acc2b90d46608c840ef66950 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 28 Jun 2021 15:42:39 +0530 Subject: [PATCH 508/550] feat: provision to make subcontracted purchase order from the production plan --- .../purchase_order_item.json | 29 ++- erpnext/manufacturing/doctype/bom/bom.json | 21 +- erpnext/manufacturing/doctype/bom/bom.py | 19 +- .../doctype/bom/bom_item_preview.html | 36 +++- erpnext/manufacturing/doctype/bom/bom_tree.js | 2 +- .../production_plan/production_plan.js | 41 +++- .../production_plan/production_plan.json | 31 ++- .../production_plan/production_plan.py | 180 +++++++++++----- .../production_plan_dashboard.py | 4 + .../production_plan/test_production_plan.py | 10 +- .../production_plan_item.json | 19 +- .../__init__.py | 0 .../production_plan_sub_assembly_item.json | 202 ++++++++++++++++++ .../production_plan_sub_assembly_item.py | 10 + .../doctype/work_order/work_order.json | 18 +- .../doctype/work_order/work_order.py | 3 +- .../work_order/work_order_preview.html | 33 +++ .../report/bom_explorer/bom_explorer.py | 11 +- .../job_card_summary/job_card_summary.js | 12 ++ .../job_card_summary/job_card_summary.json | 8 +- .../production_plan_summary/__init__.py | 0 .../production_plan_summary.js | 32 +++ .../production_plan_summary.json | 26 +++ .../production_plan_summary.py | 136 ++++++++++++ .../work_order_summary/work_order_summary.py | 5 +- erpnext/patches.txt | 1 + erpnext/patches/v13_0/update_level_in_bom.py | 30 +++ .../stock/doctype/stock_entry/stock_entry.py | 2 +- 28 files changed, 809 insertions(+), 112 deletions(-) create mode 100644 erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json create mode 100644 erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py create mode 100644 erpnext/manufacturing/doctype/work_order/work_order_preview.html create mode 100644 erpnext/manufacturing/report/production_plan_summary/__init__.py create mode 100644 erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js create mode 100644 erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json create mode 100644 erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py create mode 100644 erpnext/patches/v13_0/update_level_in_bom.py diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1dbd7c60c3..132dd1769c 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -97,6 +97,9 @@ "is_fixed_asset", "item_tax_rate", "section_break_72", + "production_plan", + "production_plan_item", + "production_plan_sub_assembly_item", "page_break" ], "fields": [ @@ -803,13 +806,37 @@ "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "production_plan", + "fieldtype": "Link", + "label": "Production Plan", + "options": "Production Plan", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "production_plan_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Item", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Sub Assembly Item", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-03-22 11:46:12.357435", + "modified": "2021-06-28 19:22:22.715365", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index f38d1b9892..7e539183b0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -36,6 +36,9 @@ "materials_section", "inspection_required", "quality_inspection_template", + "column_break_31", + "bom_level", + "section_break_33", "items", "scrap_section", "scrap_items", @@ -513,6 +516,22 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "column_break_31", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "bom_level", + "fieldtype": "Int", + "label": "BOM Level", + "read_only": 1 + }, + { + "fieldname": "section_break_33", + "fieldtype": "Section Break", + "hide_border": 1 } ], "icon": "fa fa-sitemap", @@ -520,7 +539,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-03-16 12:25:09.081968", + "modified": "2021-05-16 12:25:09.081968", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3bd1fe6c7f..c32a8a95a1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -154,6 +154,7 @@ class BOM(WebsiteGenerator): self.calculate_cost() self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) + self.set_bom_level() def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -676,6 +677,19 @@ class BOM(WebsiteGenerator): """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) + def set_bom_level(self, update=False): + levels = [] + + self.bom_level = 0 + for row in self.items: + if row.bom_no: + levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0) + + if levels: + self.bom_level = max(levels) + 1 + + if update: + self.db_set("bom_level", self.bom_level) def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': @@ -860,7 +874,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): frappe.form_dict.parent = parent if frappe.form_dict.parent: - bom_doc = frappe.get_doc("BOM", frappe.form_dict.parent) + bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent) frappe.has_permission("BOM", doc=bom_doc, throw=True) bom_items = frappe.get_all('BOM Item', @@ -871,7 +885,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): item_names = tuple(d.get('item_code') for d in bom_items) items = frappe.get_list('Item', - fields=['image', 'description', 'name', 'stock_uom', 'item_name'], + fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'], filters=[['name', 'in', item_names]]) # to get only required item dicts for bom_item in bom_items: @@ -884,6 +898,7 @@ def get_children(doctype, parent=None, is_root=False, **filters): bom_item.parent_bom_qty = bom_doc.quantity bom_item.expandable = 0 if bom_item.value in ('', None) else 1 + bom_item.image = frappe.db.escape(bom_item.image) return bom_items diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html index 6cd5f8cb3c..6088e46265 100644 --- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html +++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html @@ -1,13 +1,31 @@
    - {% if data.image %} - -
    - {% endif %} -

    - {{ __("Description") }} -

    -
    - {{ data.description }} +
    +
    + {% if data.image %} +
    + +
    + {% endif %} +
    +
    +

    + {{ __("Description") }} +

    +
    + {{ data.description }} +
    +
    +

    + {% if data.value %} + + {{ __("Open BOM {0}", [data.value.bold()]) }} + {% endif %} + {% if data.item_code %} + + {{ __("Open Item {0}", [data.item_code.bold()]) }} + {% endif %} +

    +

    diff --git a/erpnext/manufacturing/doctype/bom/bom_tree.js b/erpnext/manufacturing/doctype/bom/bom_tree.js index 185b9ed4bc..60fb377f47 100644 --- a/erpnext/manufacturing/doctype/bom/bom_tree.js +++ b/erpnext/manufacturing/doctype/bom/bom_tree.js @@ -64,7 +64,7 @@ frappe.treeview_settings["BOM"] = { if(node.is_root && node.data.value!="BOM") { frappe.model.with_doc("BOM", node.data.value, function() { var bom = frappe.model.get_doc("BOM", node.data.value); - node.data.image = bom.image || ""; + node.data.image = escape(bom.image) || ""; node.data.description = bom.description || ""; }); } diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 450aa04a73..d198a6962a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Production Plan', { setup: function(frm) { frm.custom_make_buttons = { - 'Work Order': 'Work Order', + 'Work Order': 'Work Order / Subcontract PO', 'Material Request': 'Material Request', }; @@ -68,17 +68,13 @@ frappe.ui.form.on('Production Plan', { frm.trigger("show_progress"); if (frm.doc.status !== "Completed") { - if (frm.doc.po_items && frm.doc.status !== "Closed") { - frm.add_custom_button(__("Work Order"), ()=> { - frm.trigger("make_work_order"); - }, __('Create')); - } + frm.add_custom_button(__("Work Order Tree"), ()=> { + frappe.set_route('Tree', 'Work Order', {production_plan: frm.doc.name}); + }, __('View')); - if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { - frm.add_custom_button(__("Material Request"), ()=> { - frm.trigger("make_material_request"); - }, __('Create')); - } + frm.add_custom_button(__("Production Plan Summary"), ()=> { + frappe.set_route('query-report', 'Production Plan Summary', {production_plan: frm.doc.name}); + }, __('View')); if (frm.doc.status === "Closed") { frm.add_custom_button(__("Re-open"), function() { @@ -89,6 +85,18 @@ frappe.ui.form.on('Production Plan', { frm.events.close_open_production_plan(frm, true); }, __("Status")); } + + if (frm.doc.po_items && frm.doc.status !== "Closed") { + frm.add_custom_button(__("Work Order / Subcontract PO"), ()=> { + frm.trigger("make_work_order"); + }, __('Create')); + } + + if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { + frm.add_custom_button(__("Material Request"), ()=> { + frm.trigger("make_material_request"); + }, __('Create')); + } } } @@ -233,6 +241,17 @@ frappe.ui.form.on('Production Plan', { }); }, + get_sub_assembly_items: function(frm) { + frappe.call({ + method: "get_sub_assembly_items", + freeze: true, + doc: frm.doc, + callback: function() { + refresh_field("sub_assembly_items"); + } + }); + }, + get_items_for_mr: function(frm) { if (!frm.doc.for_warehouse) { frappe.throw(__("Select warehouse for material requests")); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 1c0dde227c..84378956c6 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -32,6 +32,9 @@ "po_items", "section_break_25", "prod_plan_references", + "section_break_24", + "get_sub_assembly_items", + "sub_assembly_items", "material_request_planning", "include_non_stock_items", "include_subcontracted_items", @@ -187,7 +190,7 @@ "depends_on": "get_items_from", "fieldname": "get_items", "fieldtype": "Button", - "label": "Get Items For Work Order" + "label": "Get Finished Goods for Manufacture" }, { "fieldname": "po_items", @@ -199,7 +202,7 @@ { "fieldname": "material_request_planning", "fieldtype": "Section Break", - "label": "Material Request Planning" + "label": "Material Requirement Planning" }, { "default": "1", @@ -237,12 +240,13 @@ }, { "fieldname": "section_break_27", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_border": 1 }, { "fieldname": "mr_items", "fieldtype": "Table", - "label": "Material Request Plan Item", + "label": "Raw Materials", "no_copy": 1, "options": "Material Request Plan Item" }, @@ -337,13 +341,30 @@ "hidden": 1, "label": "Production Plan Item Reference", "options": "Production Plan Item Reference" + }, + { + "fieldname": "section_break_24", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "sub_assembly_items", + "fieldtype": "Table", + "label": "Sub Assembly Items", + "no_copy": 1, + "options": "Production Plan Sub Assembly Item" + }, + { + "fieldname": "get_sub_assembly_items", + "fieldtype": "Button", + "label": "Get Sub Assembly Items" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-24 16:59:03.643211", + "modified": "2021-06-28 20:00:33.905114", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0ede1bd4ab..38a0ee77ad 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -5,10 +5,11 @@ from __future__ import unicode_literals import frappe, json, copy from frappe import msgprint, _ -from six import string_types, iteritems +from six import iteritems from frappe.model.document import Document -from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil +from frappe.utils import (flt, cint, nowdate, add_days, comma_and, now_datetime, + ceil, get_link_to_form, getdate) from frappe.utils.csvutils import build_csv_response from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_children from erpnext.manufacturing.doctype.work_order.work_order import get_item_details @@ -349,49 +350,88 @@ class ProductionPlan(Document): @frappe.whitelist() def make_work_order(self): - wo_list = [] + wo_list, po_list = [], [] + subcontracted_po = {} + self.validate_data() + self.make_work_order_for_finished_goods(wo_list) + self.make_work_order_for_subassembly_items(wo_list, subcontracted_po) + self.make_subcontracted_purchase_order(subcontracted_po, po_list) + self.show_list_created_message('Work Order', wo_list) + self.show_list_created_message('Purchase Order', po_list) + + def make_work_order_for_finished_goods(self, wo_list): items_data = self.get_production_items() for key, item in items_data.items(): + if self.sub_assembly_items: + item['use_multi_level_bom'] = 0 + work_order = self.create_work_order(item) if work_order: wo_list.append(work_order) - if item.get("make_work_order_for_sub_assembly_items"): - work_orders = self.make_work_order_for_sub_assembly_items(item) - wo_list.extend(work_orders) + def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po): + for row in self.sub_assembly_items: + if row.type_of_manufacturing == 'Subcontract': + subcontracted_po.setdefault(row.supplier, []).append(row) + continue + + args = {} + self.prepare_args_for_sub_assembly_items(row, args) + work_order = self.create_work_order(args) + if work_order: + wo_list.append(work_order) + + def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders): + if not subcontracted_po: + return + + for supplier, po_list in subcontracted_po.items(): + po = frappe.new_doc('Purchase Order') + po.supplier = supplier + po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() + po.is_subcontracted_item = 'Yes' + for row in po_list: + args = { + 'item_code': row.production_item, + 'warehouse': row.fg_warehouse, + 'production_plan_sub_assembly_item': row.name, + 'bom': row.bom_no, + 'production_plan': self.name + } + + for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name', + 'description', 'production_plan_item']: + args[field] = row.get(field) + + po.append('items', args) + + po.set_missing_values() + po.flags.ignore_mandatory = True + po.flags.ignore_validate = True + po.insert() + purchase_orders.append(po.name) + + def show_list_created_message(self, doctype, doc_list=None): + if not doc_list: + return frappe.flags.mute_messages = False + if doc_list: + doc_list = [get_link_to_form(doctype, p) for p in doc_list] + msgprint(_("{0} created").format(comma_and(doc_list))) - if wo_list: - wo_list = ["""%s""" % \ - (p, p) for p in wo_list] - msgprint(_("{0} created").format(comma_and(wo_list))) - else : - msgprint(_("No Work Orders created")) + def prepare_args_for_sub_assembly_items(self, row, args): + for field in ["production_item", "item_name", "qty", "fg_warehouse", + "description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]: + args[field] = row.get(field) - def make_work_order_for_sub_assembly_items(self, item): - work_orders = [] - bom_data = {} - - get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty")) - - for key, data in bom_data.items(): - data.update({ - 'qty': data.get("stock_qty"), - 'production_plan': self.name, - 'use_multi_level_bom': item.get("use_multi_level_bom"), - 'company': self.company, - 'fg_warehouse': item.get("fg_warehouse"), - 'update_consumed_material_cost_in_project': 0 - }) - - work_order = self.create_work_order(data) - if work_order: - work_orders.append(work_order) - - return work_orders + args.update({ + "use_multi_level_bom": 0, + "production_plan": self.name, + "production_plan_sub_assembly_item": row.name + }) def create_work_order(self, item): from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse @@ -476,9 +516,32 @@ class ProductionPlan(Document): else : msgprint(_("No material request created")) + @frappe.whitelist() + def get_sub_assembly_items(self, manufacturing_type=None): + self.sub_assembly_items = [] + for row in self.po_items: + bom_data = [] + get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) + self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) + + self.save() + + def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): + bom_data = sorted(bom_data, key = lambda i: i.bom_level) + + for data in bom_data: + data.qty = data.stock_qty + data.production_plan_item = row.name + data.fg_warehouse = row.warehouse + data.schedule_date = row.planned_start_date + data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item + else "In House") + + self.append("sub_assembly_items", data) + @frappe.whitelist() def download_raw_materials(doc, warehouses=None): - if isinstance(doc, string_types): + if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', @@ -660,7 +723,7 @@ def get_sales_orders(self): @frappe.whitelist() def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): - if isinstance(row, string_types): + if isinstance(row, str): row = frappe._dict(json.loads(row)) company = frappe.db.escape(company) @@ -684,8 +747,11 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) -def get_warehouse_list(warehouses, warehouse_list=[]): - if isinstance(warehouses, string_types): +def get_warehouse_list(warehouses, warehouse_list=None): + if not warehouse_list: + warehouse_list = [] + + if isinstance(warehouses, str): warehouses = json.loads(warehouses) for row in warehouses: @@ -697,7 +763,7 @@ def get_warehouse_list(warehouses, warehouse_list=[]): @frappe.whitelist() def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): - if isinstance(doc, string_types): + if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) warehouse_list = [] @@ -726,6 +792,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d so_item_details = frappe._dict() for data in po_items: + if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): + data["include_exploded_items"] = 1 + planned_qty = data.get('required_qty') or data.get('planned_qty') ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty warehouse = doc.get('for_warehouse') @@ -857,23 +926,28 @@ def get_item_data(item_code): # "description": item_details.get("description") } -def get_sub_assembly_items(bom_no, bom_data, to_produce_qty): +def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): data = get_children('BOM', parent = bom_no) for d in data: if d.expandable: - key = (d.name, d.value) - if key not in bom_data: - bom_data.setdefault(key, { - 'stock_qty': 0, - 'description': d.description, - 'production_item': d.item_code, - 'item_name': d.item_name, - 'stock_uom': d.stock_uom, - 'uom': d.stock_uom, - 'bom_no': d.value - }) + parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") + bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level") + if d.value else 0) - bom_item = bom_data.get(key) - bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) + stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) + bom_data.append(frappe._dict({ + 'parent_item_code': parent_item_code, + 'description': d.description, + 'production_item': d.item_code, + 'item_name': d.item_name, + 'stock_uom': d.stock_uom, + 'uom': d.stock_uom, + 'bom_no': d.value, + 'is_sub_contracted_item': d.is_sub_contracted_item, + 'bom_level': bom_level, + 'indent': indent, + 'stock_qty': stock_qty + })) - get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"]) + if d.value: + get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py index 09ec24a67a..ca597f6327 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py @@ -9,5 +9,9 @@ def get_data(): 'label': _('Transactions'), 'items': ['Work Order', 'Material Request'] }, + { + 'label': _('Subcontract'), + 'items': ['Purchase Order'] + }, ] } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 768f99eb43..cce1bb61b6 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -169,7 +169,7 @@ class TestProductionPlan(unittest.TestCase): pln.get_items() pln.submit() - self.assertTrue(pln.po_items[0].planned_qty, 3) + self.assertTrue(pln.po_items[0].planned_qty, 3) pln.make_work_order() work_order = frappe.db.get_value('Work Order', { @@ -193,10 +193,10 @@ class TestProductionPlan(unittest.TestCase): for so_item in so_items: so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') self.assertEqual(so_wo_qty, 0.0) - + latest_plan = frappe.get_doc('Production Plan', pln.name) latest_plan.cancel() - + def test_pp_to_mr_customer_provided(self): #Material Request from Production Plan for Customer Provided create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) @@ -236,10 +236,10 @@ class TestProductionPlan(unittest.TestCase): pln.append("po_items", { "item_code": item_code, "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}), - "planned_qty": 3, - "make_work_order_for_sub_assembly_items": 1 + "planned_qty": 3 }) + pln.get_sub_assembly_items('In House') pln.submit() pln.make_work_order() diff --git a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json index 89ab7aa0a0..f829d57475 100644 --- a/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json +++ b/erpnext/manufacturing/doctype/production_plan_item/production_plan_item.json @@ -9,18 +9,17 @@ "include_exploded_items", "item_code", "bom_no", - "planned_qty", "column_break_6", - "make_work_order_for_sub_assembly_items", + "planned_qty", "warehouse", "planned_start_date", "section_break_9", "pending_qty", "ordered_qty", - "produced_qty", "column_break_17", "description", "stock_uom", + "produced_qty", "reference_section", "sales_order", "sales_order_item", @@ -32,11 +31,10 @@ ], "fields": [ { - "columns": 2, - "default": "0", + "columns": 1, + "default": "1", "fieldname": "include_exploded_items", "fieldtype": "Check", - "in_list_view": 1, "label": "Include Exploded Items" }, { @@ -80,13 +78,6 @@ "fieldname": "column_break_6", "fieldtype": "Column Break" }, - { - "default": "0", - "description": "If enabled, system will create the work order for the exploded items against which BOM is available.", - "fieldname": "make_work_order_for_sub_assembly_items", - "fieldtype": "Check", - "label": "Make Work Order for Sub Assembly Items" - }, { "fieldname": "warehouse", "fieldtype": "Link", @@ -218,7 +209,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-04-28 19:14:57.772123", + "modified": "2021-06-28 18:31:06.822168", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json new file mode 100644 index 0000000000..657ee35a85 --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -0,0 +1,202 @@ +{ + "actions": [], + "creation": "2020-12-27 16:08:36.127199", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "production_item", + "item_name", + "fg_warehouse", + "parent_item_code", + "schedule_date", + "column_break_3", + "qty", + "bom_no", + "bom_level", + "type_of_manufacturing", + "supplier", + "work_order_details_section", + "work_order", + "purchase_order", + "production_plan_item", + "column_break_7", + "produced_qty", + "received_qty", + "indent", + "section_break_19", + "uom", + "stock_uom", + "column_break_22", + "description" + ], + "fields": [ + { + "fetch_from": "sub_assembly_item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.type_of_manufacturing == \"In House\"", + "fieldname": "work_order_details_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "work_order", + "fieldtype": "Link", + "label": "Work Order", + "options": "Work Order", + "read_only": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty", + "read_only": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "read_only": 1 + }, + { + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty" + }, + { + "fieldname": "bom_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Bom No", + "options": "BOM" + }, + { + "fieldname": "production_plan_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Production Plan Item", + "read_only": 1 + }, + { + "fieldname": "parent_item_code", + "fieldtype": "Link", + "label": "Finished Good", + "options": "Item", + "read_only": 1 + }, + { + "columns": 1, + "fetch_from": "bom_no.bom_level", + "fieldname": "bom_level", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Level (BOM)", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_19", + "fieldtype": "Section Break", + "label": "Item Details" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "description", + "read_only": 1 + }, + { + "fieldname": "production_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sub Assembly Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "indent", + "fieldtype": "Int", + "label": "Indent" + }, + { + "fieldname": "fg_warehouse", + "fieldtype": "Link", + "label": "Target Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "produced_qty", + "fieldtype": "Data", + "label": "Produced Quantity", + "read_only": 1 + }, + { + "default": "In House", + "fieldname": "type_of_manufacturing", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Manufacturing Type", + "options": "In House\nSubcontract" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "mandatory_depends_on": "eval:doc.type_of_manufacturing == 'Subcontract'", + "options": "Supplier" + }, + { + "fieldname": "schedule_date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Schedule Date" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-06-28 20:10:56.296410", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Sub Assembly Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py new file mode 100644 index 0000000000..6850a2eb4e --- /dev/null +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ProductionPlanSubAssemblyItem(Document): + pass diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 44d76d2b01..3b56854aaf 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -64,11 +64,16 @@ "description", "stock_uom", "column_break2", + "references_section", "material_request", "material_request_item", "sales_order_item", + "column_break_61", "production_plan", "production_plan_item", + "production_plan_sub_assembly_item", + "parent_work_order", + "bom_level", "product_bundle_item", "amended_from" ], @@ -546,17 +551,26 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 - } + }, + { + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "label": "Production Plan Sub-assembly Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } ], "icon": "fa fa-cogs", "idx": 1, "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-06-20 15:19:14.902699", + "modified": "2021-06-28 16:19:14.902699", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", + "nsm_parent_field": "parent_work_order", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index a55b0b3df6..0a8e5329c1 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -483,7 +483,7 @@ class WorkOrder(Document): self.set('operations', []) - if not self.bom_no: + if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'): return operations = [] @@ -590,6 +590,7 @@ class WorkOrder(Document): def validate_operation_time(self): for d in self.operations: if not d.time_in_mins > 0: + print(self.bom_no, self.production_item) frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation)) def update_required_items(self): diff --git a/erpnext/manufacturing/doctype/work_order/work_order_preview.html b/erpnext/manufacturing/doctype/work_order/work_order_preview.html new file mode 100644 index 0000000000..a4bf93edef --- /dev/null +++ b/erpnext/manufacturing/doctype/work_order/work_order_preview.html @@ -0,0 +1,33 @@ +

    +
    +
    + {% if data.image %} +
    + +
    + {% endif %} +
    +
    +
    + Status {{ data.status }} +
    +
    + Qty to Produce {{ data.qty }} +
    +
    + Produced Qty {{ data.produced_qty }} +
    +
    +

    + {% if data.value %} + + {{ __("Open Work Order {0}", [data.value.bold()]) }} + {% endif %} + {% if data.item_code %} + + {{ __("Open Item {0}", [data.item_code.bold()]) }} + {% endif %} +

    +
    +
    +
    \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 48907adc5f..858b5546b0 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -20,17 +20,20 @@ def get_exploded_items(bom, data, indent=0, qty=1): fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom']) for item in exploded_items: + print(item.bom_no, indent) item["indent"] = indent data.append({ 'item_code': item.item_code, 'item_name': item.item_name, 'indent': indent, + 'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level") + if item.bom_no else ""), 'bom': item.bom_no, 'qty': item.qty * qty, 'uom': item.uom, 'description': item.description, 'scrap': item.scrap - }) + }) if item.bom_no: get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty) @@ -68,6 +71,12 @@ def get_columns(): "fieldname": "uom", "width": 100 }, + { + "label": "BOM Level", + "fieldtype": "Data", + "fieldname": "bom_level", + "width": 100 + }, { "label": "Standard Description", "fieldtype": "data", diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js index bd68db190e..cb771e4994 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.js +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.js @@ -68,6 +68,18 @@ frappe.query_reports["Job Card Summary"] = { get_data: function(txt) { return frappe.db.get_link_options('Item', txt); } + }, + { + label: __("Workstation"), + fieldname: "workstation", + fieldtype: "Link", + options: "Workstation" + }, + { + label: __("Operation"), + fieldname: "operation", + fieldtype: "Link", + options: "Operation" } ] }; diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json index 9f08fc34cb..ecf2b74bbe 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.json +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.json @@ -1,14 +1,16 @@ { - "add_total_row": 0, + "add_total_row": 1, + "columns": [], "creation": "2020-04-20 12:00:21.436619", "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 0, "is_standard": "Yes", - "letter_head": "Gadgets International", - "modified": "2020-04-20 12:00:21.436619", + "letter_head": "", + "modified": "2020-12-30 11:49:21.713561", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card Summary", diff --git a/erpnext/manufacturing/report/production_plan_summary/__init__.py b/erpnext/manufacturing/report/production_plan_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js new file mode 100644 index 0000000000..59396fef16 --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.js @@ -0,0 +1,32 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Production Plan Summary"] = { + "filters": [ + { + fieldname: "production_plan", + label: __("Production Plan"), + fieldtype: "Link", + options: "Production Plan", + reqd: 1, + get_query: function() { + return { + filters: { + "docstatus": 1 + } + }; + } + } + ], + "formatter": function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname == "document_name") { + var color = data.pending_qty > 0 ? 'red': 'green'; + value = `${data['document_name']}`; + } + + return value; + }, +}; diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json new file mode 100644 index 0000000000..33aca21a6e --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2020-12-27 11:43:39.781793", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2020-12-27 11:43:42.677584", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Production Plan Summary", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Production Plan", + "report_name": "Production Plan Summary", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + } + ] +} \ No newline at end of file diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py new file mode 100644 index 0000000000..81b1791ae8 --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -0,0 +1,136 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import flt + +def execute(filters=None): + columns, data = [], [] + data = get_data(filters) + columns = get_column(filters) + + return columns, data + +def get_data(filters): + data = [] + + order_details = {} + get_work_order_details(filters, order_details) + get_purchase_order_details(filters, order_details) + get_production_plan_item_details(filters, data, order_details) + + return data + +def get_production_plan_item_details(filters, data, order_details): + itemwise_indent = {} + + production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan")) + for row in production_plan_doc.po_items: + work_order = frappe.get_cached_value("Work Order", {"production_plan_item": row.name, + "bom_no": row.bom_no, "production_item": row.item_code}, "name") + + if row.item_code not in itemwise_indent: + itemwise_indent.setdefault(row.item_code, {}) + + data.append({ + "indent": 0, + "item_code": row.item_code, + "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "qty": row.planned_qty, + "document_type": "Work Order", + "document_name": work_order, + "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"), + "produced_qty": order_details.get((work_order, row.item_code)).get("produced_qty"), + "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code)).get("produced_qty")) + }) + + get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details) + +def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details): + for item in production_plan_doc.sub_assembly_items: + if row.name == item.production_plan_item: + subcontracted_item = (item.type_of_manufacturing == 'Subcontract') + + if subcontracted_item: + docname = frappe.get_cached_value("Purchase Order Item", + {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "parent") + else: + docname = frappe.get_cached_value("Work Order", + {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name") + + data.append({ + "indent": 1, + "item_code": item.production_item, + "item_name": item.item_name, + "qty": item.qty, + "document_type": "Work Order" if not subcontracted_item else "Purchase Order", + "document_name": docname, + "bom_level": item.bom_level, + "produced_qty": order_details.get((docname, item.production_item)).get("produced_qty"), + "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item)).get("produced_qty")) + }) + +def get_work_order_details(filters, order_details): + for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")}, + fields=["name", "produced_qty", "production_plan", "production_item"]): + order_details.setdefault((row.name, row.production_item), row) + +def get_purchase_order_details(filters, order_details): + for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")}, + fields=["parent", "received_qty as produced_qty", "item_code"]): + order_details.setdefault((row.parent, row.item_code), row) + +def get_column(filters): + return [ + { + "label": "Finished Good", + "fieldtype": "Link", + "fieldname": "item_code", + "width": 300, + "options": "Item" + }, + { + "label": "Item Name", + "fieldtype": "data", + "fieldname": "item_name", + "width": 100 + }, + { + "label": "Document Type", + "fieldtype": "Link", + "fieldname": "document_type", + "width": 150, + "options": "DocType" + }, + { + "label": "Document Name", + "fieldtype": "Dynamic Link", + "fieldname": "document_name", + "width": 150 + }, + { + "label": "BOM Level", + "fieldtype": "Int", + "fieldname": "bom_level", + "width": 100 + }, + { + "label": "Order Qty", + "fieldtype": "Float", + "fieldname": "qty", + "width": 120 + }, + { + "label": "Received Qty", + "fieldtype": "Float", + "fieldname": "produced_qty", + "width": 160 + }, + { + "label": "Pending Qty", + "fieldtype": "Float", + "fieldname": "pending_qty", + "width": 110 + } + ] diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index fb047b230c..612dad0bf5 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -19,7 +19,7 @@ def execute(filters=None): return columns, data, None, chart_data def get_data(filters): - query_filters = {"docstatus": 1} + query_filters = {"docstatus": ("<", 2)} fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty", "planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"] @@ -62,7 +62,8 @@ def get_chart_based_on_status(data): "Not Started": 0, "In Process": 0, "Stopped": 0, - "Completed": 0 + "Completed": 0, + "Draft": 0 } for d in data: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c93f7a7ed9..7ea2c137b1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -293,3 +293,4 @@ erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.update_response_by_variance erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details +erpnext.patches.v13_0.update_level_in_bom #1234sswef diff --git a/erpnext/patches/v13_0/update_level_in_bom.py b/erpnext/patches/v13_0/update_level_in_bom.py new file mode 100644 index 0000000000..0d03c42e98 --- /dev/null +++ b/erpnext/patches/v13_0/update_level_in_bom.py @@ -0,0 +1,30 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + for document in ["bom", "bom_item", "bom_explosion_item"]: + frappe.reload_doc('manufacturing', 'doctype', document) + + frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1") + + bom_list = frappe.db.sql_list("""select name from `tabBOM` bom + where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item` + where parent=bom.name and ifnull(bom_no, '')!='')""") + + count = 0 + while(count < len(bom_list)): + for parent_bom in get_parent_boms(bom_list[count]): + bom_doc = frappe.get_cached_doc("BOM", parent_bom) + bom_doc.set_bom_level(update=True) + bom_list.append(parent_bom) + count += 1 + +def get_parent_boms(bom_no): + return frappe.db.sql_list(""" + select distinct bom_item.parent from `tabBOM Item` bom_item + where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM' + and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1) + """, bom_no) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 90b81ddb1d..9dc63784d7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -72,7 +72,7 @@ class StockEntry(StockController): self.validate_with_material_request() self.validate_batch() self.validate_inspection() - self.validate_fg_completed_qty() + # self.validate_fg_completed_qty() self.validate_difference_account() self.set_job_card_data() self.set_purpose_for_stock_entry() From 1b5f3cf6059e8c164f87ecc24f3b6d0048f3289c Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 15 Jul 2021 19:30:05 +0530 Subject: [PATCH 509/550] ci: make semgrep ignore existing errors (#26516) --- .../semgrep_rules/frappe_correctness.yml | 2 - .github/workflows/semgrep.yml | 38 ++++++------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml index faab3344a6..d9603e89aa 100644 --- a/.github/helper/semgrep_rules/frappe_correctness.yml +++ b/.github/helper/semgrep_rules/frappe_correctness.yml @@ -98,8 +98,6 @@ rules: languages: [python] severity: WARNING paths: - exclude: - - test_*.py include: - "*/**/doctype/*" diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 389524e968..701c5c7cbe 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -1,34 +1,20 @@ name: Semgrep on: - pull_request: - branches: - - develop - - version-13-hotfix - - version-13-pre-release + pull_request: { } + push: + branches: ["develop"] + jobs: semgrep: name: Frappe Linter runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Setup python3 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Setup semgrep - run: | - python -m pip install -q semgrep - git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q - - - name: Semgrep errors - run: | - files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) - [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files - semgrep --config="r/python.lang.correctness" --quiet --error $files - - - name: Semgrep warnings - run: | - files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) - [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files + - uses: actions/checkout@v2 + - uses: returntocorp/semgrep-action@v1 + env: + SEMGREP_TIMEOUT: 120 + with: + config: >- + r/python.lang.correctness + .github/helper/semgrep_rules From df5c2b9aaf7b39a558c428074ab0c517fa11964e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Jul 2021 14:08:58 +0530 Subject: [PATCH 510/550] fix: FG item not fetched in manufacture entry --- .../doctype/work_order/test_work_order.py | 54 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 22 +++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 68de0b29d3..bf1ccb7159 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -513,6 +513,60 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_batch_size_for_fg_item(self): + fg_item = "Test Batch Size Item For BOM 3" + rm1 = "Test Batch Size Item RM 1 For BOM 3" + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]: + item_args = { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + } + + if item == fg_item: + item_args['has_batch_no'] = 1 + item_args['create_new_batch'] = 1 + item_args['batch_number_series'] = 'TBSI3.#####' + + make_item(item, item_args) + + bom_name = frappe.db.get_value("BOM", + {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + + if not bom_name: + bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom.save() + bom.submit() + bom_name = bom.name + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), + qty=30, do_not_save = True) + work_order.batch_size = 10 + work_order.insert() + work_order.submit() + self.assertEqual(work_order.has_batch_no, 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + self.assertEqual(row.qty, 10) + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + def test_partial_material_consumption(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9dc63784d7..50fcb3957b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1090,13 +1090,13 @@ class StockEntry(StockController): "is_finished_item": 1 } - if self.work_order and self.pro_doc.has_batch_no: + if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', + 'make_serial_no_batch_from_work_order', cache=True)): self.set_batchwise_finished_goods(args, item) else: - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) def set_batchwise_finished_goods(self, args, item): - qty = flt(self.fg_completed_qty) filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, @@ -1105,7 +1105,17 @@ class StockEntry(StockController): fields = ["qty_to_produce as qty", "produced_qty", "name"] - for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): + data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + + if not data: + self.add_finished_goods(args, item) + else: + self.add_batchwise_finished_good(data, args, item) + + def add_batchwise_finished_good(self, data, args, item): + qty = flt(self.fg_completed_qty) + + for row in data: batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue @@ -1121,9 +1131,9 @@ class StockEntry(StockController): args["qty"] = fg_qty args["batch_no"] = row.name - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) - def add_finisged_goods(self, args, item): + def add_finished_goods(self, args, item): self.add_to_stock_entry_detail({ item.name: args }, bom_no = self.bom_no) From a6d80dcc2a650cf46b41d3fbb959b4c5da54693e Mon Sep 17 00:00:00 2001 From: Ankush Date: Fri, 16 Jul 2021 13:01:57 +0530 Subject: [PATCH 511/550] chore: disable semgrep on push events (#26523) --- .github/workflows/semgrep.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 701c5c7cbe..e27b406df0 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -2,8 +2,6 @@ name: Semgrep on: pull_request: { } - push: - branches: ["develop"] jobs: semgrep: From a0df79ee8c1e7d251e9f5f24c11bbb21b22621da Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 16 Jul 2021 13:07:39 +0530 Subject: [PATCH 512/550] chore: Added change log for v13.7.0 --- erpnext/change_log/v13/v13_7_0.md | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 erpnext/change_log/v13/v13_7_0.md diff --git a/erpnext/change_log/v13/v13_7_0.md b/erpnext/change_log/v13/v13_7_0.md new file mode 100644 index 0000000000..589f610b93 --- /dev/null +++ b/erpnext/change_log/v13/v13_7_0.md @@ -0,0 +1,69 @@ +# Version 13.7.0 Release Notes + +### Features & Enhancements +- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133)) +- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415)) +- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454)) +- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240)) +- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432)) + +### Fixes +- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277)) +- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218)) +- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488)) +- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358)) +- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394)) +- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287)) +- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253)) +- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497)) +- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353)) +- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333)) +- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331)) +- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271)) +- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927)) +- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886)) +- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368)) +- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486)) +- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231)) +- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829)) +- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462)) +- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388)) +- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493)) +- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298)) +- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507)) +- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259)) +- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) +- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165)) +- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468)) +- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472)) +- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158)) +- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279)) +- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252)) +- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405)) +- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323)) +- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470)) +- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320)) +- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466)) +- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334)) +- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411)) +- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382)) +- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350)) +- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300)) +- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255)) +- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312)) +- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290)) +- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332)) +- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284)) +- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226)) +- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349)) +- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241)) +- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367)) +- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391)) +- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244)) +- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458)) +- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509)) +- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303)) +- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213)) +- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278)) +- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461)) +- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285)) \ No newline at end of file From 0c6ca09e06faf53da276af64b8a16a441bf7e6ef Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Jul 2021 16:32:23 +0530 Subject: [PATCH 513/550] fix: added patch to fix missing FG item --- erpnext/patches.txt | 1 + .../add_missing_fg_item_for_stock_entry.py | 110 ++++++++++++++++++ .../repost_item_valuation.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 4 + 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 29376f00a1..f63c7edea2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -291,3 +291,4 @@ erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef +erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py new file mode 100644 index 0000000000..48999e6f99 --- /dev/null +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -0,0 +1,110 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.utils import cstr, flt, cint +from erpnext.stock.stock_ledger import make_sl_entries +from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + +def execute(): + if not frappe.db.has_column('Work Order', 'has_batch_no'): + return + + if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): + return + + frappe.reload_doc('manufacturing', 'doctype', 'work_order') + filters = { + 'docstatus': 1, + 'produced_qty': ('>', 0), + 'creation': ('>=', '2021-06-29 00:00:00'), + 'has_batch_no': 1 + } + + fields = ['name', 'production_item'] + + work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)] + + if not work_orders: + return + + repost_stock_entries = [] + stock_entries = frappe.db.sql_list(''' + SELECT + se.name + FROM + `tabStock Entry` se + WHERE + se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders} + and not exists( + select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 + ) + Order BY + se.posting_date, se.posting_time + '''.format(work_orders=tuple(work_orders))) + + if stock_entries: + print('Length of stock entries', len(stock_entries)) + + for stock_entry in stock_entries: + doc = frappe.get_doc('Stock Entry', stock_entry) + doc.set_work_order_details() + doc.load_items_from_bom() + doc.calculate_rate_and_amount() + set_expense_account(doc) + doc.make_batches('t_warehouse') + + if doc.docstatus == 0: + doc.save() + else: + repost_stock_entry(doc) + repost_stock_entries.append(doc) + + for repost_doc in repost_stock_entries: + repost_future_sle_and_gle(repost_doc) + +def set_expense_account(doc): + for row in doc.items: + if row.is_finished_item and not row.expense_account: + row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account') + +def repost_stock_entry(doc): + doc.db_update() + for child_row in doc.items: + if child_row.is_finished_item: + child_row.db_update() + + sl_entries = [] + finished_item_row = doc.get_finished_item_row() + get_sle_for_target_warehouse(doc, sl_entries, finished_item_row) + + if sl_entries: + try: + make_sl_entries(sl_entries, True) + except Exception: + print(f'SLE entries not posted for the stock entry {doc.name}') + traceback = frappe.get_traceback() + frappe.log_error(traceback) + +def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row): + for d in doc.get('items'): + if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name: + sle = doc.get_sl_entries(d, { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate) + }) + + sle.recalculate_rate = 1 + sl_entries.append(sle) + +def repost_future_sle_and_gle(doc): + args = frappe._dict({ + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "company": doc.company + }) + + create_repost_item_valuation_entry(args) \ No newline at end of file diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 55f2ebb224..5f31d9caf0 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -133,6 +133,6 @@ def repost_entries(): def get_repost_item_valuation_entries(): return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` - WHERE status != 'Completed' and creation <= %s and docstatus = 1 + WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc """, now(), as_dict=1) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c9838d75f1..872b1d0516 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -719,6 +719,10 @@ class StockEntry(StockController): frappe.throw(_("Multiple items cannot be marked as finished item")) if self.purpose == "Manufacture": + if not finished_items: + frappe.throw(_('Finished Good has not set in the stock entry {0}') + .format(self.name)) + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) From 56e4a88956cea13cebd542df6f1cf49aae9094ee Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Jul 2021 16:32:23 +0530 Subject: [PATCH 514/550] fix: added patch to fix missing FG item --- erpnext/patches.txt | 1 + .../add_missing_fg_item_for_stock_entry.py | 110 ++++++++++++++++++ .../repost_item_valuation.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 4 + 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7ea2c137b1..0ae8130ad4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -294,3 +294,4 @@ erpnext.patches.v13_0.update_response_by_variance erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef +erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py new file mode 100644 index 0000000000..48999e6f99 --- /dev/null +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -0,0 +1,110 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.utils import cstr, flt, cint +from erpnext.stock.stock_ledger import make_sl_entries +from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + +def execute(): + if not frappe.db.has_column('Work Order', 'has_batch_no'): + return + + if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): + return + + frappe.reload_doc('manufacturing', 'doctype', 'work_order') + filters = { + 'docstatus': 1, + 'produced_qty': ('>', 0), + 'creation': ('>=', '2021-06-29 00:00:00'), + 'has_batch_no': 1 + } + + fields = ['name', 'production_item'] + + work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)] + + if not work_orders: + return + + repost_stock_entries = [] + stock_entries = frappe.db.sql_list(''' + SELECT + se.name + FROM + `tabStock Entry` se + WHERE + se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders} + and not exists( + select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 + ) + Order BY + se.posting_date, se.posting_time + '''.format(work_orders=tuple(work_orders))) + + if stock_entries: + print('Length of stock entries', len(stock_entries)) + + for stock_entry in stock_entries: + doc = frappe.get_doc('Stock Entry', stock_entry) + doc.set_work_order_details() + doc.load_items_from_bom() + doc.calculate_rate_and_amount() + set_expense_account(doc) + doc.make_batches('t_warehouse') + + if doc.docstatus == 0: + doc.save() + else: + repost_stock_entry(doc) + repost_stock_entries.append(doc) + + for repost_doc in repost_stock_entries: + repost_future_sle_and_gle(repost_doc) + +def set_expense_account(doc): + for row in doc.items: + if row.is_finished_item and not row.expense_account: + row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account') + +def repost_stock_entry(doc): + doc.db_update() + for child_row in doc.items: + if child_row.is_finished_item: + child_row.db_update() + + sl_entries = [] + finished_item_row = doc.get_finished_item_row() + get_sle_for_target_warehouse(doc, sl_entries, finished_item_row) + + if sl_entries: + try: + make_sl_entries(sl_entries, True) + except Exception: + print(f'SLE entries not posted for the stock entry {doc.name}') + traceback = frappe.get_traceback() + frappe.log_error(traceback) + +def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row): + for d in doc.get('items'): + if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name: + sle = doc.get_sl_entries(d, { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate) + }) + + sle.recalculate_rate = 1 + sl_entries.append(sle) + +def repost_future_sle_and_gle(doc): + args = frappe._dict({ + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "company": doc.company + }) + + create_repost_item_valuation_entry(args) \ No newline at end of file diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 55f2ebb224..5f31d9caf0 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -133,6 +133,6 @@ def repost_entries(): def get_repost_item_valuation_entries(): return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` - WHERE status != 'Completed' and creation <= %s and docstatus = 1 + WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc """, now(), as_dict=1) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 50fcb3957b..fcb6f0f4c2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -719,6 +719,10 @@ class StockEntry(StockController): frappe.throw(_("Multiple items cannot be marked as finished item")) if self.purpose == "Manufacture": + if not finished_items: + frappe.throw(_('Finished Good has not set in the stock entry {0}') + .format(self.name)) + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) From e079a1bb33cb1eb322cbd42635800c9eb0832836 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 16 Jul 2021 15:41:33 +0550 Subject: [PATCH 515/550] bumped to version 13.7.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 0c96d325c2..1166549628 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.6.0' +__version__ = '13.7.0' def get_default_company(user=None): '''Get default company for user''' From 70a72524697bb676806e54737631aaa2da945b6f Mon Sep 17 00:00:00 2001 From: Ankush Date: Sat, 17 Jul 2021 12:48:40 +0530 Subject: [PATCH 516/550] chore: update CODEOWNERS (#26536) --- CODEOWNERS | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7cf65a7a73..219b6bb782 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,16 +3,33 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -manufacturing/ @rohitwaghchaure @marination -accounts/ @deepeshgarg007 @nextchamp-saqib -loan_management/ @deepeshgarg007 @rohitwaghchaure -pos* @nextchamp-saqib @rohitwaghchaure -assets/ @nextchamp-saqib @deepeshgarg007 -stock/ @marination @rohitwaghchaure -buying/ @marination @deepeshgarg007 -hr/ @Anurag810 @rohitwaghchaure -projects/ @hrwX @nextchamp-saqib -support/ @hrwX @marination -healthcare/ @ruchamahabal @marination -erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib -requirements.txt @gavindsouza +erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 +erpnext/assets/ @nextchamp-saqib @deepeshgarg007 +erpnext/erpnext_integrations/ @nextchamp-saqib +erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 +erpnext/regional @nextchamp-saqib @deepeshgarg007 +erpnext/selling @nextchamp-saqib @deepeshgarg007 +erpnext/support/ @nextchamp-saqib @deepeshgarg007 +pos* @nextchamp-saqib + +erpnext/buying/ @marination @rohitwaghchaure @ankush +erpnext/e_commerce/ @marination +erpnext/maintenance/ @marination @rohitwaghchaure +erpnext/manufacturing/ @marination @rohitwaghchaure @ankush +erpnext/portal/ @marination +erpnext/quality_management/ @marination @rohitwaghchaure +erpnext/shopping_cart/ @marination +erpnext/stock/ @marination @rohitwaghchaure @ankush + +erpnext/crm/ @ruchamahabal +erpnext/education/ @ruchamahabal +erpnext/healthcare/ @ruchamahabal +erpnext/hr/ @ruchamahabal +erpnext/non_profit/ @ruchamahabal +erpnext/payroll @ruchamahabal +erpnext/projects/ @ruchamahabal + +erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination + +.github/ @surajshetty3416 @ankush +requirements.txt @gavindsouza From 2686d04800fe3d7450e9eb4a1f8bbdb070c66aa3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 18 Jul 2021 17:54:35 +0530 Subject: [PATCH 517/550] fix: Typo and remove duplicate function --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index c375dacb8b..cbb3dc881f 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -183,16 +183,6 @@ def make_item_tax_template(company_name, template): doc.insert(ignore_permissions=True) return doc -def make_tax_category(tax_category): - """ Make tax category based on title if not already created """ - doctype = 'Tax Category' - if not frappe.db.exists(doctype, tax_category['title']): - tax_category['doctype'] = doctype - doc = frappe.get_doc(tax_category) - doc.flags.ignore_links = True - doc.flags.ignore_validate = True - doc.insert(ignore_permissions=True) - def get_or_create_account(company_name, account): """ Check if account already exists. If not, create it. @@ -284,7 +274,7 @@ def get_or_create_tax_group(company_name, root_type): return tax_group_name -def make_tax_catgory(tax_category): +def make_tax_category(tax_category): doctype = 'Tax Category' if isinstance(tax_category, str): tax_category = {'title': tax_category} From b24a149dbc5bbf9e5213d283cce76fd678ae2710 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 19 Jul 2021 14:37:12 +0530 Subject: [PATCH 518/550] test: Updated test case for Eway bill --- .../sales_invoice/test_sales_invoice.py | 27 +++++++++++++++++++ erpnext/regional/india/utils.py | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index fe531d3b22..6d31c12be6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1908,6 +1908,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) + self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) + self.assertEqual(data['billLists'][0]['fromStateCode'],27) def test_einvoice_submission_without_irn(self): # init @@ -2061,6 +2063,30 @@ def make_test_address_for_ewaybill(): address.save() + if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'): + address = frappe.get_doc({ + "address_line1": "_Test Dispatch Address Line 1", + "address_title": "_Test Dispatch-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 0, + "phone": "+910000000000", + "gstin": "07AAACC1206D1ZI", + "gst_state": "Delhi", + "gst_state_number": "07", + "pincode": "1100101" + }).insert() + + address.append("links", { + "link_doctype": "Company", + "link_name": "_Test Company" + }) + + address.save() + def make_test_transporter_for_ewaybill(): if not frappe.db.exists('Supplier', '_Test Transporter'): frappe.get_doc({ @@ -2099,6 +2125,7 @@ def make_sales_invoice_for_ewaybill(): si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" si.customer_address = "_Test Customer-Address for Eway bill-Shipping" + si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping" si.vehicle_no = "KA12KA1234" si.gst_category = "Registered Regular" si.mode_of_transport = 'Road' diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 61f5a0578e..fbe47d0532 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -432,7 +432,7 @@ def get_ewb_data(dt, dn): billing_address = frappe.get_doc('Address', doc.customer_address) #added dispatch address - dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) + dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address shipping_address = frappe.get_doc('Address', doc.shipping_address_name) data = get_address_details(data, doc, company_address, billing_address, dispatch_address) @@ -524,7 +524,7 @@ def get_gstins_for_company(company): def get_address_details(data, doc, company_address, billing_address, dispatch_address): data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address') - data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Company Address') + data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address') if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: data.toGstin = 'URP' From 80e269887d5e23845a48723a73573249233c0ee6 Mon Sep 17 00:00:00 2001 From: Ankush Date: Mon, 19 Jul 2021 20:42:44 +0530 Subject: [PATCH 519/550] fix(ux): item description should fall back to name (#26339) Don't set item description = item code from front end. This is already being set to item_name in before_insert and item_name is better fallback than item code for description. Also fixed wrong condition for erasing description while duplicating item. --- erpnext/stock/doctype/item/item.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 568f0ef451..87bd9e61fe 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -100,10 +100,11 @@ frappe.ui.form.on("Item", { frm.add_custom_button(__('Duplicate'), function() { var new_item = frappe.model.copy_doc(frm.doc); - if(new_item.item_name===new_item.item_code) { + // Duplicate item could have different name, causing "copy paste" error. + if (new_item.item_name===new_item.item_code) { new_item.item_name = null; } - if(new_item.description===new_item.description) { + if (new_item.item_code===new_item.description || new_item.item_code===new_item.description) { new_item.description = null; } frappe.set_route('Form', 'Item', new_item.name); @@ -186,8 +187,6 @@ frappe.ui.form.on("Item", { item_code: function(frm) { if(!frm.doc.item_name) frm.set_value("item_name", frm.doc.item_code); - if(!frm.doc.description) - frm.set_value("description", frm.doc.item_code); }, is_stock_item: function(frm) { From c077314568eec088758e0d1292b97cb21f253d52 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 20 Jul 2021 09:57:18 +0530 Subject: [PATCH 520/550] fix: Pass doc and other parameters to properly prefill information - while creating customer from form dashboard --- erpnext/public/js/utils/customer_quick_entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js index efb8dd9d5c..d2c5c721cc 100644 --- a/erpnext/public/js/utils/customer_quick_entry.js +++ b/erpnext/public/js/utils/customer_quick_entry.js @@ -1,8 +1,8 @@ frappe.provide('frappe.ui.form'); frappe.ui.form.CustomerQuickEntryForm = class CustomerQuickEntryForm extends frappe.ui.form.QuickEntryForm { - constructor(doctype, after_insert) { - super(doctype, after_insert); + constructor(doctype, after_insert, init_callback, doc, force) { + super(doctype, after_insert, init_callback, doc, force); this.skip_redirect_on_error = true; } From 47f200a70d050ef44258ba7a927d067e2bd8fd8d Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 20 Jul 2021 16:13:49 +0530 Subject: [PATCH 521/550] chore: Update stale.yml reduce `daysUntilStale` & `daysUntilClose` to keep the contributors active. --- .github/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index dabc66eb73..9322ae87bf 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,11 +1,11 @@ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 30 +daysUntilStale: 15 # Number of days of inactivity before a stale Issue or Pull Request is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 7 +daysUntilClose: 3 # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: From 013b3526398311366d103a44c3261eb276e25d9c Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 20 Jul 2021 21:03:11 +0530 Subject: [PATCH 522/550] fix: Price list rate not fetched for return sales invoice fixed (#26559) Co-authored-by: Subin Tom Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- erpnext/stock/get_item_details.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 4657700dbb..cf52803fca 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -74,9 +74,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru update_party_blanket_order(args, out) - if not doc or cint(doc.get('is_return')) == 0: - # get price list rate only if the invoice is not a credit or debit note - get_price_list_rate(args, item, out) + + get_price_list_rate(args, item, out) if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) From a758071532c900a6cdd657d0a4c16df454a1abc7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 21 Jul 2021 00:24:09 +0530 Subject: [PATCH 523/550] feat(Non Profit): API Endpoint to update halted Razorpay subscriptions (#26427) * feat: Update Subscription Activated field to Subscription Status to accomodate Halted status * feat: API Endpoint to halt Razorpay subscription * fix: sider * fix: validation message * test: halted razorpay subscription --- erpnext/non_profit/doctype/member/member.json | 16 ++-- erpnext/non_profit/doctype/member/member.py | 4 +- .../doctype/membership/membership.py | 92 ++++++++++++++----- .../doctype/membership/test_membership.py | 56 +++++++++-- erpnext/patches.txt | 1 + ...date_subscription_status_in_memberships.py | 9 ++ 6 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 erpnext/patches/v13_0/update_subscription_status_in_memberships.py diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index f190cfae75..7c1baf1a8d 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -26,7 +26,7 @@ "razorpay_details_section", "subscription_id", "customer_id", - "subscription_activated", + "subscription_status", "column_break_21", "subscription_start", "subscription_end" @@ -151,12 +151,6 @@ "fieldname": "column_break_21", "fieldtype": "Column Break" }, - { - "default": "0", - "fieldname": "subscription_activated", - "fieldtype": "Check", - "label": "Subscription Activated" - }, { "fieldname": "subscription_start", "fieldtype": "Date", @@ -166,11 +160,17 @@ "fieldname": "subscription_end", "fieldtype": "Date", "label": "Subscription End" + }, + { + "fieldname": "subscription_status", + "fieldtype": "Select", + "label": "Subscription Status", + "options": "\nActive\nHalted" } ], "image_field": "image", "links": [], - "modified": "2020-11-09 12:12:10.174647", + "modified": "2021-07-11 14:27:26.368039", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 30be585e9a..67828d6efc 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -84,7 +84,9 @@ def create_member(user_details): "email_id": user_details.email, "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, - "subscription_id": user_details.subscription_id or None + "customer_id": user_details.customer_id or None, + "subscription_id": user_details.subscription_id or None, + "subscription_status": user_details.subscription_status or "" }) member.insert(ignore_permissions=True) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index e8ae6187b7..b584116df3 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -196,11 +196,14 @@ def make_invoice(membership, member, plan, settings): return invoice -def get_member_based_on_subscription(subscription_id, email): - members = frappe.get_all("Member", filters={ - "subscription_id": subscription_id, - "email_id": email - }, order_by="creation desc") +def get_member_based_on_subscription(subscription_id, email=None, customer_id=None): + filters = {"subscription_id": subscription_id} + if email: + filters.update({"email_id": email}) + if customer_id: + filters.update({"customer_id": customer_id}) + + members = frappe.get_all("Member", filters=filters, order_by="creation desc") try: return frappe.get_doc("Member", members[0]["name"]) @@ -209,8 +212,6 @@ def get_member_based_on_subscription(subscription_id, email): def verify_signature(data, endpoint="Membership"): - if frappe.flags.in_test or os.environ.get("CI"): - return True signature = frappe.request.headers.get("X-Razorpay-Signature") settings = frappe.get_doc("Non Profit Settings") @@ -225,16 +226,7 @@ def verify_signature(data, endpoint="Membership"): @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) - try: - verify_signature(data) - except Exception as e: - log = frappe.log_error(e, "Membership Webhook Verification Error") - notify_failure(log) - return { "status": "Failed", "reason": e} - - if isinstance(data, six.string_types): - data = json.loads(data) - data = frappe._dict(data) + data = process_request_data(data) subscription = data.payload.get("subscription", {}).get("entity", {}) subscription = frappe._dict(subscription) @@ -281,7 +273,7 @@ def trigger_razorpay_subscription(*args, **kwargs): # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) - member.subscription_activated = 1 + member.subscription_status = "Active" member.flags.ignore_mandatory = True member.save() @@ -294,9 +286,67 @@ def trigger_razorpay_subscription(*args, **kwargs): message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) notify_failure(log) - return { "status": "Failed", "reason": e} + return {"status": "Failed", "reason": e} - return { "status": "Success" } + return {"status": "Success"} + + +@frappe.whitelist(allow_guest=True) +def update_halted_razorpay_subscription(*args, **kwargs): + """ + When all retries have been exhausted, Razorpay moves the subscription to the halted state. + The customer has to manually retry the charge or change the card linked to the subscription, + for the subscription to move back to the active state. + """ + if frappe.request: + data = frappe.request.get_data(as_text=True) + data = process_request_data(data) + elif frappe.flags.in_test: + data = kwargs.get("data") + data = frappe._dict(data) + else: + return + + if not data.event == "subscription.halted": + return + + subscription = data.payload.get("subscription", {}).get("entity", {}) + subscription = frappe._dict(subscription) + + try: + member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id) + if not member: + frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id)) + + member.subscription_status = "Halted" + member.flags.ignore_mandatory = True + member.save() + + if subscription.get("notes"): + member = get_additional_notes(member, subscription) + + except Exception as e: + message = "{0}\n\n{1}".format(e, frappe.get_traceback()) + log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name)) + notify_failure(log) + return {"status": "Failed", "reason": e} + + return {"status": "Success"} + + +def process_request_data(data): + try: + verify_signature(data) + except Exception as e: + log = frappe.log_error(e, "Membership Webhook Verification Error") + notify_failure(log) + return {"status": "Failed", "reason": e} + + if isinstance(data, six.string_types): + data = json.loads(data) + data = frappe._dict(data) + + return data def get_company_for_memberships(): @@ -362,4 +412,4 @@ def set_expired_status(): `tabMembership` SET `status` = 'Expired' WHERE `status` not in ('Cancelled') AND `to_date` < %s - """, (nowdate())) \ No newline at end of file + """, (nowdate())) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index 31da792e53..0f5a9bed82 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -6,6 +6,7 @@ import unittest import frappe import erpnext from erpnext.non_profit.doctype.member.member import create_member +from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription from frappe.utils import nowdate, add_months class TestMembership(unittest.TestCase): @@ -13,11 +14,16 @@ class TestMembership(unittest.TestCase): plan = setup_membership() # make test member - self.member_doc = create_member(frappe._dict({ - 'fullname': "_Test_Member", - 'email': "_test_member_erpnext@example.com", - 'plan_id': plan.name - })) + self.member_doc = create_member( + frappe._dict({ + "fullname": "_Test_Member", + "email": "_test_member_erpnext@example.com", + "plan_id": plan.name, + "subscription_id": "sub_DEX6xcJ1HSW4CR", + "customer_id": "cust_C0WlbKhp3aLA7W", + "subscription_status": "Active" + }) + ) self.member_doc.make_customer_and_link() self.member = self.member_doc.name @@ -51,6 +57,20 @@ class TestMembership(unittest.TestCase): "to_date": add_months(nowdate(), 3), }) + def test_halted_memberships(self): + make_membership(self.member, { + "from_date": add_months(nowdate(), 2), + "to_date": add_months(nowdate(), 3) + }) + + self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active") + payload = get_subscription_payload() + update_halted_razorpay_subscription(data=payload) + self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted") + + def tearDown(self): + frappe.db.rollback() + def set_config(key, value): frappe.db.set_value("Non Profit Settings", None, key, value) @@ -115,4 +135,28 @@ def setup_membership(): else: plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") - return plan \ No newline at end of file + return plan + +def get_subscription_payload(): + return { + "entity": "event", + "account_id": "acc_BFQ7uQEaa7j2z7", + "event": "subscription.halted", + "contains": [ + "subscription" + ], + "payload": { + "subscription": { + "entity": { + "id": "sub_DEX6xcJ1HSW4CR", + "entity": "subscription", + "plan_id": "_rzpy_test_milythm", + "customer_id": "cust_C0WlbKhp3aLA7W", + "status": "halted", + "notes": { + "Important": "Notes for Internal Reference" + }, + } + } + } + } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0ae8130ad4..8debf86432 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -295,3 +295,4 @@ erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry +erpnext.patches.v13_0.update_subscription_status_in_memberships diff --git a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py new file mode 100644 index 0000000000..28e650e9ce --- /dev/null +++ b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py @@ -0,0 +1,9 @@ +import frappe + +def execute(): + if frappe.db.exists('DocType', 'Member'): + frappe.reload_doc('Non Profit', 'doctype', 'Member') + + if frappe.db.has_column('Member', 'subscription_activated'): + frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1') + frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated') \ No newline at end of file From 28d52c4a9521759625bb13e5ea0c50c9112e4fed Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 21 Jul 2021 19:54:06 +0530 Subject: [PATCH 524/550] chore: remove warning rules semgrep-action doesn't consider severity, hence ignoring these rules for now. --- .github/helper/semgrep_rules/security.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml index 5a5098bf50..8b21979208 100644 --- a/.github/helper/semgrep_rules/security.yml +++ b/.github/helper/semgrep_rules/security.yml @@ -8,18 +8,3 @@ rules: dynamic content. Avoid it or use safe_eval(). languages: [python] severity: ERROR - -- id: frappe-sqli-format-strings - patterns: - - pattern-inside: | - @frappe.whitelist() - def $FUNC(...): - ... - - pattern-either: - - pattern: frappe.db.sql("..." % ...) - - pattern: frappe.db.sql(f"...", ...) - - pattern: frappe.db.sql("...".format(...), ...) - message: | - Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines - languages: [python] - severity: WARNING From f9da88cb15c831e50dc823acaeacbaa822a7fabf Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 19 Jul 2021 21:45:33 +0530 Subject: [PATCH 525/550] fix: Additional discount calculations in Invoices --- .../public/js/controllers/taxes_and_totals.js | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1de9ec1a7d..b5aa6265be 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -34,9 +34,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.set_value(item.doctype, item.name, "rate", item_rate); }, - calculate_taxes_and_totals: function(update_paid_amount) { + calculate_taxes_and_totals: async function(update_paid_amount) { this.discount_amount_applied = false; - this._calculate_taxes_and_totals(); + await this._calculate_taxes_and_totals(); this.calculate_discount_amount(); // Advance calculation applicable to Sales /Purchase Invoice @@ -72,19 +72,20 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - _calculate_taxes_and_totals: function() { - frappe.run_serially([ - () => this.validate_conversion_rate(), - () => this.calculate_item_values(), - () => this.update_item_tax_map(), - () => this.initialize_taxes(), - () => this.determine_exclusive_rate(), - () => this.calculate_net_total(), - () => this.calculate_taxes(), - () => this.manipulate_grand_total_for_inclusive_tax(), - () => this.calculate_totals(), - () => this._cleanup() - ]); + _calculate_taxes_and_totals: async function() { + this.validate_conversion_rate(); + this.calculate_item_values(); + await this.update_item_tax_map(); + }, + + _calculate_tax_values : function() { + this.initialize_taxes(); + this.determine_exclusive_rate(); + this.calculate_net_total(); + this.calculate_taxes(); + this.manipulate_grand_total_for_inclusive_tax(); + this.calculate_totals(); + this._cleanup(); }, validate_conversion_rate: function() { @@ -105,7 +106,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, calculate_item_values: function() { - var me = this; + let me = this; if (!this.discount_amount_applied) { $.each(this.frm.doc["items"] || [], function(i, item) { frappe.model.round_floats_in(item); @@ -266,7 +267,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); }, - update_item_tax_map: function() { + update_item_tax_map: async function() { let me = this; let item_codes = []; let item_rates = {}; @@ -301,6 +302,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }); } + + me._calculate_tax_values(); } }); } From 50b188214d03dc8d2113e8567f8a11dd16a221fa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Jul 2021 12:41:48 +0530 Subject: [PATCH 526/550] revert: Client side handling for Dynamic GST Rates --- .../public/js/controllers/taxes_and_totals.js | 54 ++----------------- 1 file changed, 4 insertions(+), 50 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index b5aa6265be..263570cb66 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -34,9 +34,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.set_value(item.doctype, item.name, "rate", item_rate); }, - calculate_taxes_and_totals: async function(update_paid_amount) { + calculate_taxes_and_totals: function(update_paid_amount) { this.discount_amount_applied = false; - await this._calculate_taxes_and_totals(); + this._calculate_taxes_and_totals(); this.calculate_discount_amount(); // Advance calculation applicable to Sales /Purchase Invoice @@ -65,20 +65,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.refresh_fields(); }, - calculate_discount_amount: function(){ + calculate_discount_amount: function() { if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { this.set_discount_amount(); this.apply_discount_amount(); } }, - _calculate_taxes_and_totals: async function() { + _calculate_taxes_and_totals: function() { this.validate_conversion_rate(); this.calculate_item_values(); - await this.update_item_tax_map(); - }, - - _calculate_tax_values : function() { this.initialize_taxes(); this.determine_exclusive_rate(); this.calculate_net_total(); @@ -267,48 +263,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); }, - update_item_tax_map: async function() { - let me = this; - let item_codes = []; - let item_rates = {}; - let item_tax_templates = {}; - - $.each(this.frm.doc.items || [], function(i, item) { - if (item.item_code) { - // Use combination of name and item code in case same item is added multiple times - item_codes.push([item.item_code, item.name]); - item_rates[item.name] = item.net_rate; - item_tax_templates[item.name] = item.item_tax_template; - } - }); - - if (item_codes.length) { - return this.frm.call({ - method: "erpnext.stock.get_item_details.get_item_tax_info", - args: { - company: me.frm.doc.company, - tax_category: cstr(me.frm.doc.tax_category), - item_codes: item_codes, - item_rates: item_rates, - item_tax_templates: item_tax_templates - }, - callback: function(r) { - if (!r.exc) { - $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { - item.item_tax_template = r.message[item.name].item_tax_template; - item.item_tax_rate = r.message[item.name].item_tax_rate; - me.add_taxes_from_item_tax_template(item.item_tax_rate); - } - }); - } - - me._calculate_tax_values(); - } - }); - } - }, - add_taxes_from_item_tax_template: function(item_tax_map) { let me = this; From 72eb72f66f64518f859de681adbb4de63a235d28 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Jul 2021 15:45:04 +0530 Subject: [PATCH 527/550] fix: Add update item tax template method back --- erpnext/public/js/controllers/transaction.js | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b3af3d67ea..6eb6775b28 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1787,6 +1787,46 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ ]); }, + update_item_tax_map: function() { + let me = this; + let item_codes = []; + let item_rates = {}; + let item_tax_templates = {}; + + $.each(this.frm.doc.items || [], function(i, item) { + if (item.item_code) { + // Use combination of name and item code in case same item is added multiple times + item_codes.push([item.item_code, item.name]); + item_rates[item.name] = item.net_rate; + item_tax_templates[item.name] = item.item_tax_template; + } + }); + + if (item_codes.length) { + return this.frm.call({ + method: "erpnext.stock.get_item_details.get_item_tax_info", + args: { + company: me.frm.doc.company, + tax_category: cstr(me.frm.doc.tax_category), + item_codes: item_codes, + item_rates: item_rates, + item_tax_templates: item_tax_templates + }, + callback: function(r) { + if (!r.exc) { + $.each(me.frm.doc.items || [], function(i, item) { + if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { + item.item_tax_template = r.message[item.name].item_tax_template; + item.item_tax_rate = r.message[item.name].item_tax_rate; + me.add_taxes_from_item_tax_template(item.item_tax_rate); + } + }); + } + } + }); + } + }, + item_tax_template: function(doc, cdt, cdn) { var me = this; if(me.frm.updating_party_details) return; From 9fa92c912b484c82b49e77b9c00527ecd165d6df Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Jul 2021 17:02:05 +0530 Subject: [PATCH 528/550] fix: Revert refresh field --- erpnext/public/js/controllers/taxes_and_totals.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 263570cb66..53d5278bbf 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -587,8 +587,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail); }); } - - this.frm.refresh_fields(); }, set_discount_amount: function() { From 9ab18b534141c4a4bdf2a4c48541febaa55cbe23 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Jul 2021 23:15:15 +0530 Subject: [PATCH 529/550] fix: add company change trigger --- erpnext/public/js/controllers/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 6eb6775b28..5475383759 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -826,9 +826,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.run_serially([ () => me.frm.script_manager.trigger("currency"), + () => me.update_item_tax_map(), () => me.apply_default_taxes(), - () => me.apply_pricing_rule(), - () => me.calculate_taxes_and_totals() + () => me.apply_pricing_rule() ]); } } From 4ee657178478a4fa6b882cdbfb5898d64bda6563 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 22 Jul 2021 13:13:46 +0530 Subject: [PATCH 530/550] fix: SQL error on fetching RM in production plan (#26592) * fix: SQL error on fetching RM in production plan * refactor: avoid passing by reference and mutations --- .../production_plan/production_plan.py | 15 ++++-------- .../production_plan/test_production_plan.py | 23 ++++++++++++++++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 38a0ee77ad..6a024f275a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -747,9 +747,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) -def get_warehouse_list(warehouses, warehouse_list=None): - if not warehouse_list: - warehouse_list = [] +def get_warehouse_list(warehouses): + warehouse_list = [] if isinstance(warehouses, str): warehouses = json.loads(warehouses) @@ -761,23 +760,19 @@ def get_warehouse_list(warehouses, warehouse_list=None): else: warehouse_list.append(row.get("warehouse")) + return warehouse_list + @frappe.whitelist() def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - warehouse_list = [] if warehouses: - get_warehouse_list(warehouses, warehouse_list) - - if warehouse_list: - warehouses = list(set(warehouse_list)) + warehouses = list(set(get_warehouse_list(warehouses))) if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: warehouses.remove(doc.get("for_warehouse")) - warehouse_list = None - doc['mr_items'] = [] po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index cce1bb61b6..93e6d7a97f 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -10,7 +10,7 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests +from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list class TestProductionPlan(unittest.TestCase): def setUp(self): @@ -251,6 +251,27 @@ class TestProductionPlan(unittest.TestCase): pln.cancel() frappe.delete_doc("Production Plan", pln.name) + def test_get_warehouse_list_group(self): + """Check if required warehouses are returned""" + warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]' + + warehouses = set(get_warehouse_list(warehouse_json)) + expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"} + + missing_warehouse = expected_warehouses - warehouses + + self.assertTrue(len(missing_warehouse) == 0, + msg=f"Following warehouses were expected {', '.join(missing_warehouse)}") + + def test_get_warehouse_list_single(self): + warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]' + + warehouses = set(get_warehouse_list(warehouse_json)) + expected_warehouses = {"_Test Scrap Warehouse - _TC", } + + self.assertEqual(warehouses, expected_warehouses) + + def create_production_plan(**args): args = frappe._dict(args) From 0d968fabfecafe544fe5700922eae206a4cb5816 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sat, 17 Jul 2021 14:42:38 -0400 Subject: [PATCH 531/550] fix: missing parameter 'country' --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 4fd8413d83..8456b49c8e 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -391,5 +391,5 @@ def set_default_accounts(company): }) company.save() - install_country_fixtures(company.name) + install_country_fixtures(company.name, company.country) company.create_default_tax_template() From ce3e877c4076ca0ae231788c8baa825041cd9f60 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 22 Jul 2021 16:10:58 +0530 Subject: [PATCH 532/550] fix: incorrect bom name (bp #26600) --- erpnext/manufacturing/doctype/bom/bom.js | 7 ++++--- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index c56668840e..3f50b41be1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -83,7 +83,7 @@ frappe.ui.form.on("BOM", { if (!frm.doc.__islocal && frm.doc.docstatus<2) { frm.add_custom_button(__("Update Cost"), function() { - frm.events.update_cost(frm); + frm.events.update_cost(frm, true); }); frm.add_custom_button(__("Browse BOM"), function() { frappe.route_options = { @@ -318,14 +318,15 @@ frappe.ui.form.on("BOM", { }) }, - update_cost: function(frm) { + update_cost: function(frm, save_doc=false) { return frappe.call({ doc: frm.doc, method: "update_cost", freeze: true, args: { update_parent: true, - from_child_bom:false + save: save_doc, + from_child_bom: false }, callback: function(r) { refresh_field("items"); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 9da461f497..8692f3dc48 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -330,7 +330,7 @@ class BOM(WebsiteGenerator): frappe.get_doc("BOM", bom).update_cost(from_child_bom=True) if not from_child_bom: - frappe.msgprint(_("Cost Updated")) + frappe.msgprint(_("Cost Updated"), alert=True) def update_parent_cost(self): if self.total_cost: From 7551bcf421f134e3510ded387326d273ef6add82 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 22 Jul 2021 17:25:51 +0550 Subject: [PATCH 533/550] bumped to version 13.7.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 1166549628..a181c2d42c 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.7.0' +__version__ = '13.7.1' def get_default_company(user=None): '''Get default company for user''' From 4128aa762887ff7a82621420c7f4c8bfa637de76 Mon Sep 17 00:00:00 2001 From: Anurag0911 <67339426+Anurag0911@users.noreply.github.com> Date: Fri, 23 Jul 2021 16:52:42 +0530 Subject: [PATCH 534/550] fix: syntax error (#26610) Removed "," at erpnext/public/js/controllers/taxes_and_totals.js:87 --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index e1f71f796a..a495a9b0c1 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -84,7 +84,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.manipulate_grand_total_for_inclusive_tax(); this.calculate_totals(); this._cleanup(); - }, + } validate_conversion_rate() { this.frm.doc.conversion_rate = flt(this.frm.doc.conversion_rate, (cur_frm) ? precision("conversion_rate") : 9); From fac88a3329425fe075e0114fd3ca4ef8f613cf85 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Fri, 23 Jul 2021 21:23:48 +0530 Subject: [PATCH 535/550] fix: Supplier Invoice Importer fix --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 2 +- erpnext/controllers/accounts_controller.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 8456b49c8e..4fd8413d83 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -391,5 +391,5 @@ def set_default_accounts(company): }) company.save() - install_country_fixtures(company.name, company.country) + install_country_fixtures(company.name) company.create_default_tax_template() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4c313c43a7..cdd865ac4a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1112,8 +1112,11 @@ class AccountsController(TransactionBase): for d in self.get("payment_schedule"): if d.invoice_portion: d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) - d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) + d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount')) d.outstanding = d.payment_amount + elif not d.invoice_portion: + d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount')) + def set_due_date(self): due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] From 057a0a98428b138b20646b820e7ab27c99bc5f22 Mon Sep 17 00:00:00 2001 From: Ankush Date: Sun, 25 Jul 2021 12:49:05 +0530 Subject: [PATCH 536/550] ci: auto backport squashed commits based on labels (#26622) --- .github/workflows/backport.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 7c6b8432b8..cc98f4544f 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,16 +1,25 @@ name: Backport on: - pull_request: + pull_request_target: types: - closed - labeled jobs: - backport: - runs-on: ubuntu-18.04 - name: Backport + main: + runs-on: ubuntu-latest steps: - - name: Backport - uses: tibdex/backport@v1 + - name: Checkout Actions + uses: actions/checkout@v2 with: - github_token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + repository: "ankush/backport" + path: ./actions + ref: develop + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run backport + uses: ./actions/backport + with: + token: ${{secrets.BACKPORT_BOT_TOKEN}} + labelsToAdd: "backport" + title: "{{originalTitle}}" From 96caae1f56a6bdd0ab70e8bc3bb686cc31c76ccc Mon Sep 17 00:00:00 2001 From: Ankush Date: Sun, 25 Jul 2021 13:01:21 +0530 Subject: [PATCH 537/550] fix: wrong operation time in Work Order (#26613) (#26617) * fix: wrong operation time in Work Order Top level item time operation was not considering the BOM.quantity Co-authored-by: Ankush Menat Co-authored-by: rohitwaghchaure --- .../doctype/work_order/work_order.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0a8e5329c1..69812c7452 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -487,21 +487,20 @@ class WorkOrder(Document): return operations = [] - if not self.use_multi_level_bom: - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) - else: + + if self.use_multi_level_bom: bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() - bom_traversal = list(reversed(bom_tree.level_order_traversal())) - bom_traversal.append(bom_tree) # add operation on top level item last + bom_traversal = reversed(bom_tree.level_order_traversal()) - for d in bom_traversal: - if d.is_bom: - operations.extend(_get_operations(d.name, qty=d.exploded_qty)) + for node in bom_traversal: + if node.is_bom: + operations.extend(_get_operations(node.name, qty=node.exploded_qty)) - for correct_index, operation in enumerate(operations, start=1): - operation.idx = correct_index + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) + for correct_index, operation in enumerate(operations, start=1): + operation.idx = correct_index self.set('operations', operations) self.calculate_time() From cd12d95a246bcf5c49eda78fe8ce4fa9c90e6b76 Mon Sep 17 00:00:00 2001 From: Ankush Date: Sun, 25 Jul 2021 13:10:50 +0530 Subject: [PATCH 538/550] fix: incorrect amount in work order required items table. (#26585) * fix: amount in work order not equal to rate * qty * fix: patch for amount in work order required items --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- erpnext/patches.txt | 1 + .../v13_0/update_amt_in_work_order_required_items.py | 10 ++++++++++ 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v13_0/update_amt_in_work_order_required_items.py diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8692f3dc48..bc092ef14f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -774,7 +774,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item.image, bom.project, bom_item.rate, - bom_item.amount, + sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount, item.stock_uom, item.item_group, item.allow_alternative_item, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 69812c7452..282b5d0afe 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -655,7 +655,7 @@ class WorkOrder(Document): for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): self.append('required_items', { 'rate': item.rate, - 'amount': item.amount, + 'amount': item.rate * item.qty, 'operation': item.operation or operation, 'item_code': item.item_code, 'item_name': item.item_name, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8debf86432..a029627ab1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -296,3 +296,4 @@ erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships +erpnext.patches.v13_0.update_amt_in_work_order_required_items diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py new file mode 100644 index 0000000000..eae5ff60b9 --- /dev/null +++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + """ Correct amount in child table of required items table.""" + + frappe.reload_doc("manufacturing", "doctype", "work_order") + frappe.reload_doc("manufacturing", "doctype", "work_order_item") + + frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""") + From f4701c174a79b848ad30325036832235458fec85 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 25 Jul 2021 19:46:20 +0530 Subject: [PATCH 539/550] fix: Exchange rate revaluation posting date and precision fixes --- .../exchange_rate_revaluation.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 56193216a2..e94875f2d7 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -99,10 +99,12 @@ class ExchangeRateRevaluation(Document): sum(debit) - sum(credit) as balance from `tabGL Entry` where account in (%s) + and posting_date <= %s + and is_cancelled = 0 group by account, party_type, party having sum(debit) != sum(credit) order by account - """ % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) + """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1) return account_details @@ -143,9 +145,9 @@ class ExchangeRateRevaluation(Document): "party_type": d.get("party_type"), "party": d.get("party"), "account_currency": d.get("account_currency"), - "balance": d.get("balance_in_account_currency"), - dr_or_cr: abs(d.get("balance_in_account_currency")), - "exchange_rate":d.get("new_exchange_rate"), + "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), + dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), + "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, }) @@ -154,9 +156,9 @@ class ExchangeRateRevaluation(Document): "party_type": d.get("party_type"), "party": d.get("party"), "account_currency": d.get("account_currency"), - "balance": d.get("balance_in_account_currency"), - reverse_dr_or_cr: abs(d.get("balance_in_account_currency")), - "exchange_rate": d.get("current_exchange_rate"), + "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), + reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), + "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name }) @@ -185,9 +187,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N account_details = {} company_currency = erpnext.get_company_currency(company) - balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False) + balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False) if balance: - balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party) + balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party) current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate From c485d5c3b7503136ad03cc29dd69317e2ad17696 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 25 Jul 2021 19:46:50 +0530 Subject: [PATCH 540/550] fix: Ignore GL Entry on cancel --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index e94875f2d7..c8d5737d75 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document): if not (self.company and self.posting_date): frappe.throw(_("Please select Company and Posting Date to getting entries")) + def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry') + @frappe.whitelist() def check_journal_entry_condition(self): total_debit = frappe.db.get_value("Journal Entry Account", { From 67273b955123778e4018aeb8dcde01bf1a60cbd4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 25 Jul 2021 21:26:22 +0530 Subject: [PATCH 541/550] fix: Convert null values to empty string on grouping --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index c8d5737d75..f2b0a8c08a 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -104,7 +104,7 @@ class ExchangeRateRevaluation(Document): where account in (%s) and posting_date <= %s and is_cancelled = 0 - group by account, party_type, party + group by account, NULLIF(party_type,''), NULLIF(party,'') having sum(debit) != sum(credit) order by account """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1) From 2d439f235522b55e236a7f31e407bf56f89d153e Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Mon, 26 Jul 2021 11:08:31 +0530 Subject: [PATCH 542/550] chore: Updated CODEOWNERS --- CODEOWNERS | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 219b6bb782..330a8dba01 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,13 +21,13 @@ erpnext/quality_management/ @marination @rohitwaghchaure erpnext/shopping_cart/ @marination erpnext/stock/ @marination @rohitwaghchaure @ankush -erpnext/crm/ @ruchamahabal +erpnext/crm/ @ruchamahabal @pateljannat erpnext/education/ @ruchamahabal -erpnext/healthcare/ @ruchamahabal -erpnext/hr/ @ruchamahabal +erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand +erpnext/hr/ @ruchamahabal @pateljannat erpnext/non_profit/ @ruchamahabal -erpnext/payroll @ruchamahabal -erpnext/projects/ @ruchamahabal +erpnext/payroll @ruchamahabal @pateljannat +erpnext/projects/ @ruchamahabal @pateljannat erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination From cbddedab7bf2fc7637b861214c3373a742da830b Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 26 Jul 2021 12:54:35 +0530 Subject: [PATCH 543/550] fix: included company in Link Document Type filters for contact (#26576) --- erpnext/hooks.py | 3 ++- erpnext/public/js/contact.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 erpnext/public/js/contact.js diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9717bb9b17..59b011d1a9 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -25,7 +25,8 @@ doctype_js = { "Address": "public/js/address.js", "Communication": "public/js/communication.js", "Event": "public/js/event.js", - "Newsletter": "public/js/newsletter.js" + "Newsletter": "public/js/newsletter.js", + "Contact": "public/js/contact.js" } override_doctype_class = { diff --git a/erpnext/public/js/contact.js b/erpnext/public/js/contact.js new file mode 100644 index 0000000000..41a0e8a9f9 --- /dev/null +++ b/erpnext/public/js/contact.js @@ -0,0 +1,16 @@ + + +frappe.ui.form.on("Contact", { + refresh(frm) { + frm.set_query('link_doctype', "links", function() { + return { + query: "frappe.contacts.address_and_contact.filter_dynamic_link_doctypes", + filters: { + fieldtype: ["in", ["HTML", "Text Editor"]], + fieldname: ["in", ["contact_html", "company_description"]], + } + }; + }); + frm.refresh_field("links"); + } +}); From 00fd319531438cd6eb31db5b80584ccdf17687d3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 19 Jul 2021 14:36:54 +0530 Subject: [PATCH 544/550] fix: Add missing cess amount in GSTR-3B report --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 641520437f..6de228fbc7 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -322,6 +322,9 @@ class GSTR3BReport(Document): inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100) + if self.invoice_cess.get(inv): + self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2) + self.set_inter_state_supply(inter_state_supply_details) def set_supplies_liable_to_reverse_charge(self): From 19589b1e21083c42486e70ea405e8d29fdb75b1d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Jul 2021 13:25:53 +0530 Subject: [PATCH 545/550] fix: GST Reports timeout issue --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 5 ++--- erpnext/regional/report/gstr_1/gstr_1.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 641520437f..6a61ae2b42 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -214,9 +214,8 @@ class GSTR3BReport(Document): for d in item_details: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, - sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details - if i.item_code == d.item_code and i.parent == d.parent)) + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) + self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) if d.is_nil_exempt and d.item_code not in self.is_nil_exempt: self.is_nil_exempt.append(d.item_code) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index cfcb8c3444..b81fa810fe 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -217,9 +217,8 @@ class Gstr1Report(object): for d in items: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, - sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items - if i.item_code == d.item_code and i.parent == d.parent)) + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) + self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) item_tax_rate = {} From 16feebf6856600cca43a4c1a524fbd98b0de9be7 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Mon, 26 Jul 2021 18:17:30 +0530 Subject: [PATCH 546/550] Update CODEOWNERS chore: update code owner for education module --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 330a8dba01..a4a14de1b8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -22,7 +22,7 @@ erpnext/shopping_cart/ @marination erpnext/stock/ @marination @rohitwaghchaure @ankush erpnext/crm/ @ruchamahabal @pateljannat -erpnext/education/ @ruchamahabal +erpnext/education/ @ruchamahabal @pateljannat erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand erpnext/hr/ @ruchamahabal @pateljannat erpnext/non_profit/ @ruchamahabal From ae9d1d9617015b6e2714a9fcd027a2957053d99a Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 27 Jul 2021 09:40:12 +0530 Subject: [PATCH 547/550] fix: Salary component account filter (#26604) * fix: salary component account filter * fix: cleanup --- .../doctype/salary_component/salary_component.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index dbf75140ac..e9e6f81862 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -4,11 +4,18 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { frm.set_query("account", "accounts", function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; + let d = frappe.get_doc(cdt, cdn); + + let root_type = "Liability"; + if (frm.doc.type == "Deduction") { + root_type = "Expense"; + } + return { filters: { "is_group": 0, - "company": d.company + "company": d.company, + "root_type": root_type } }; }); From 34353df48c3bf58f61157ebb58accca886fc0b1a Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 27 Jul 2021 09:47:44 +0530 Subject: [PATCH 548/550] fix: sales pipeline graph issue (#26626) --- erpnext/selling/page/sales_funnel/sales_funnel.css | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.css b/erpnext/selling/page/sales_funnel/sales_funnel.css index 89e904fcfc..455d37cb23 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.css +++ b/erpnext/selling/page/sales_funnel/sales_funnel.css @@ -1,3 +1,4 @@ .funnel-wrapper { margin: 15px; + width: 100%; } \ No newline at end of file From 6b482ebb0f8b8ce6d7efe4704f853c4508954c45 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 23 Jul 2021 16:40:45 +0530 Subject: [PATCH 549/550] fix: serial no and batch validation --- erpnext/controllers/stock_controller.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2526e6df0e..17bd7354f9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -53,12 +53,17 @@ class StockController(AccountsController): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos for d in self.get("items"): if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no: - serial_nos = get_serial_nos(d.serial_no) - for serial_no_data in frappe.get_all("Serial No", - filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]): - if serial_no_data.batch_no != d.batch_no: + serial_nos = frappe.get_all("Serial No", + fields=["batch_no", "name", "warehouse"], + filters={ + "name": ("in", get_serial_nos(d.serial_no)) + } + ) + + for row in serial_nos: + if row.warehouse and row.batch_no != d.batch_no: frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") - .format(d.idx, serial_no_data.name, d.batch_no)) + .format(d.idx, row.name, d.batch_no)) if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") From c8d7a8c781f6c448fd872427d611ffab70c136db Mon Sep 17 00:00:00 2001 From: Ankush Date: Tue, 27 Jul 2021 16:39:38 +0530 Subject: [PATCH 550/550] fix: reload manufacturing setting before patch (#26641) --- erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py index 48999e6f99..d7ad1fc696 100644 --- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -10,6 +10,7 @@ def execute(): if not frappe.db.has_column('Work Order', 'has_batch_no'): return + frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): return @@ -107,4 +108,4 @@ def repost_future_sle_and_gle(doc): "company": doc.company }) - create_repost_item_valuation_entry(args) \ No newline at end of file + create_repost_item_valuation_entry(args)