From 363ed9ccba3f848908113e6d728735a1c894aec8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 5 Feb 2022 14:06:18 +0100 Subject: [PATCH 01/36] =?UTF-8?q?revert:=20BU=20Schl=C3=BCssel=20(a21f76f)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- erpnext/regional/report/datev/datev.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index beac7ed65cd..92a10c288f8 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -343,8 +343,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): /* against number or, if empty, party against number */ %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', - /* disable automatic VAT deduction */ - '40' as 'BU-Schlüssel', + '' as 'BU-Schlüssel', gl.posting_date as 'Belegdatum', gl.voucher_no as 'Belegfeld 1', From b2755f6fdddd3e1b0a305b57c18651c98fee8f7e Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 7 Mar 2022 13:02:08 +0530 Subject: [PATCH 02/36] feat: Include child item group products in Item Group Page & cleanup - Added 'Include descendants' checkbox, which will pull child item group products too - Build item group filters in query engine file - Include logic in filter engine - Clean up Website section of Item Group page (UX) - Add util to fetch child item groups including self --- erpnext/e_commerce/api.py | 1 - .../e_commerce/product_data_engine/filters.py | 22 ++++++--- .../e_commerce/product_data_engine/query.py | 48 ++++++++----------- .../setup/doctype/item_group/item_group.json | 31 +++++++++--- .../setup/doctype/item_group/item_group.py | 8 +++- 5 files changed, 69 insertions(+), 41 deletions(-) diff --git a/erpnext/e_commerce/api.py b/erpnext/e_commerce/api.py index 43cb36ca2e2..84554ae7d0f 100644 --- a/erpnext/e_commerce/api.py +++ b/erpnext/e_commerce/api.py @@ -48,7 +48,6 @@ def get_product_filter_data(query_args=None): sub_categories = [] if item_group: - field_filters['item_group'] = item_group sub_categories = get_child_groups_for_website(item_group, immediate=True) engine = ProductQuery() diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py index c4a3cb9fbef..89d4ffdff48 100644 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ b/erpnext/e_commerce/product_data_engine/filters.py @@ -14,6 +14,8 @@ class ProductFiltersBuilder: self.item_group = item_group def get_field_filters(self): + from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website + if not self.item_group and not self.doc.enable_field_filters: return @@ -25,18 +27,26 @@ class ProductFiltersBuilder: fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] for df in fields: - item_filters, item_or_filters = {}, [] + item_filters, item_or_filters = {"published_in_website": 1}, [] link_doctype_values = self.get_filtered_link_doctype_records(df) if df.fieldtype == "Link": if self.item_group: - item_or_filters.extend([ - ["item_group", "=", self.item_group], - ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups - ]) + include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants") + if include_child: + include_groups = get_child_groups_for_website(self.item_group, include_self=True) + include_groups = [x.name for x in include_groups] + item_or_filters.extend([ + ["item_group", "in", include_groups], + ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups + ]) + else: + item_or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] # consider website item groups + ]) # Get link field values attached to published items - item_filters['published_in_website'] = 1 item_values = frappe.get_all( "Item", fields=[df.fieldname], diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index cfc3c7b357c..5c92e3d98ad 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -46,10 +46,10 @@ class ProductQuery: self.filter_with_discount = bool(fields.get("discount")) result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 - website_item_groups = self.get_website_item_group_results(item_group, website_item_groups) - if fields: self.build_fields_filters(fields) + if item_group: + self.build_item_group_filters(item_group) if search_term: self.build_search_filters(search_term) if self.settings.hide_variants: @@ -61,8 +61,6 @@ class ProductQuery: else: result, count = self.query_items(start=start) - result = self.combine_web_item_group_results(item_group, result, website_item_groups) - # sort combined results by ranking result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) @@ -167,6 +165,25 @@ class ProductQuery: # `=` will be faster than `IN` for most cases self.filters.append([field, "=", values]) + def build_item_group_filters(self, item_group): + "Add filters for Item group page and include Website Item Groups." + from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website + item_group_filters = [] + + item_group_filters.append(["Website Item", "item_group", "=", item_group]) + # Consider Website Item Groups + item_group_filters.append(["Website Item Group", "item_group", "=", item_group]) + + if frappe.db.get_value("Item Group", item_group, "include_descendants"): + # include child item group's items as well + # eg. Group Node A, will show items of child 1 and child 2 as well + # on it's web page + include_groups = get_child_groups_for_website(item_group, include_self=True) + include_groups = [x.name for x in include_groups] + item_group_filters.append(["Website Item", "item_group", "in", include_groups]) + + self.or_filters.extend(item_group_filters) + def build_search_filters(self, search_term): """Query search term in specified fields @@ -190,19 +207,6 @@ class ProductQuery: for field in search_fields: self.or_filters.append([field, "like", search]) - def get_website_item_group_results(self, item_group, website_item_groups): - """Get Web Items for Item Group Page via Website Item Groups.""" - if item_group: - website_item_groups = frappe.db.get_all( - "Website Item", - fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], - filters=[ - ["Website Item Group", "item_group", "=", item_group], - ["published", "=", 1] - ] - ) - return website_item_groups - def add_display_details(self, result, discount_list, cart_items): """Add price and availability details in result.""" for item in result: @@ -278,16 +282,6 @@ class ProductQuery: return [] - def combine_web_item_group_results(self, item_group, result, website_item_groups): - """Combine results with 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) - - return result - def filter_results_by_discount(self, fields, result): if fields and fields.get("discount"): discount_percent = frappe.utils.flt(fields["discount"][0]) diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index 3e0680f4f51..a090c8d76c5 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -20,12 +20,14 @@ "sec_break_taxes", "taxes", "sb9", - "show_in_website", "route", - "weightage", - "slideshow", "website_title", "description", + "show_in_website", + "include_descendants", + "column_break_16", + "weightage", + "slideshow", "website_specifications", "website_filters_section", "filter_fields", @@ -111,7 +113,7 @@ }, { "default": "0", - "description": "Check this if you want to show in website", + "description": "Make Item Group visible in website", "fieldname": "show_in_website", "fieldtype": "Check", "label": "Show in Website" @@ -124,6 +126,7 @@ "unique": 1 }, { + "depends_on": "show_in_website", "fieldname": "weightage", "fieldtype": "Int", "label": "Weightage" @@ -186,6 +189,8 @@ "report_hide": 1 }, { + "collapsible": 1, + "depends_on": "show_in_website", "fieldname": "website_filters_section", "fieldtype": "Section Break", "label": "Website Filters" @@ -203,9 +208,21 @@ "options": "Website Attribute" }, { + "depends_on": "show_in_website", "fieldname": "website_title", "fieldtype": "Data", "label": "Title" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Include Website Items belonging to child Item Groups", + "fieldname": "include_descendants", + "fieldtype": "Check", + "label": "Include Descendants" } ], "icon": "fa fa-sitemap", @@ -214,11 +231,12 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2021-02-18 13:40:30.049650", + "modified": "2022-03-07 09:44:47.561532", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", "name_case": "Title Case", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_item_group", "owner": "Administrator", "permissions": [ @@ -285,5 +303,6 @@ "search_fields": "parent_item_group", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 4f92240c84f..5c7194baf68 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -111,7 +111,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): from erpnext.stock.doctype.item.item import validate_item_default_company_links validate_item_default_company_links(self.item_group_defaults) -def get_child_groups_for_website(item_group_name, immediate=False): +def get_child_groups_for_website(item_group_name, immediate=False, include_self=False): """Returns child item groups *excluding* passed group.""" item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) filters = { @@ -123,6 +123,12 @@ def get_child_groups_for_website(item_group_name, immediate=False): if immediate: filters["parent_item_group"] = item_group_name + if include_self: + filters.update({ + "lft": [">=", item_group.lft], + "rgt": ["<=", item_group.rgt] + }) + return frappe.get_all( "Item Group", filters=filters, From 9ace7d606cb5a63da8434200e9811d550de5cb5a Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Mon, 7 Mar 2022 16:53:59 +0530 Subject: [PATCH 03/36] fix: Ignore missing customer group while fetching price list --- erpnext/accounts/party.py | 11 +++++------ erpnext/accounts/test_party.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 erpnext/accounts/test_party.py diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index d6f6c5bcb69..b4438527158 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -151,7 +151,7 @@ def set_contact_details(party_details, party, party_type): def set_other_values(party_details, party, party_type): # copy - if party_type=="Customer": + if party_type == "Customer": to_copy = ["customer_name", "customer_group", "territory", "language"] else: to_copy = ["supplier_name", "supplier_group", "language"] @@ -170,12 +170,11 @@ def get_default_price_list(party): return party.default_price_list if party.doctype == "Customer": - price_list = frappe.get_cached_value("Customer Group", - party.customer_group, "default_price_list") - if price_list: - return price_list + try: + return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list") + except frappe.exceptions.DoesNotExistError: + return - return None def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list diff --git a/erpnext/accounts/test_party.py b/erpnext/accounts/test_party.py new file mode 100644 index 00000000000..f7a1a858ab8 --- /dev/null +++ b/erpnext/accounts/test_party.py @@ -0,0 +1,16 @@ +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.accounts.party import get_default_price_list + + +class PartyTestCase(FrappeTestCase): + def test_get_default_price_list_should_return_none_for_invalid_group(self): + customer = frappe.get_doc({ + 'doctype': 'Customer', + 'customer_name': 'test customer', + }).insert(ignore_permissions=True, ignore_mandatory=True) + customer.customer_group = None + customer.save() + price_list = get_default_price_list(customer) + assert price_list is None From 66f20209f657ae1825bc9971dbd19f186e454d50 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Mar 2022 15:57:54 +0530 Subject: [PATCH 04/36] perf(asset): fetch only distinct depreciable assets --- erpnext/assets/doctype/asset/depreciation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 874fb630f87..6e042422101 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -23,7 +23,7 @@ def post_depreciation_entries(date=None): frappe.db.commit() def get_depreciable_assets(date): - return frappe.db.sql_list("""select a.name + return frappe.db.sql_list("""select distinct a.name from tabAsset a, `tabDepreciation Schedule` ds where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 and a.status in ('Submitted', 'Partially Depreciated') From 3507cf59852c6d6814f6650b4b1a6e6584e69aa6 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Mar 2022 12:24:57 +0530 Subject: [PATCH 05/36] test: Test include_descendants in Item Group Product Listing - Also made include_descendants field's visibility dependant on show_in_website --- .../test_item_group_product_data_engine.py | 53 ++++++++++++++++--- .../setup/doctype/item_group/item_group.json | 3 +- .../templates/pages/non_profit/__init__.py | 0 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 erpnext/templates/pages/non_profit/__init__.py diff --git a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py index f0f7918d00e..6549ba692af 100644 --- a/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py +++ b/erpnext/e_commerce/product_data_engine/test_item_group_product_data_engine.py @@ -13,8 +13,7 @@ test_dependencies = ["Item", "Item Group"] class TestItemGroupProductDataEngine(unittest.TestCase): "Test Products & Sub-Category Querying for Product Listing on Item Group Page." - @classmethod - def setUpClass(cls): + def setUp(self): item_codes = [ ("Test Mobile A", "_Test Item Group B"), ("Test Mobile B", "_Test Item Group B"), @@ -28,8 +27,10 @@ class TestItemGroupProductDataEngine(unittest.TestCase): if not frappe.db.exists("Website Item", {"item_code": item_code}): create_regular_web_item(item_code, item_args=item_args) - @classmethod - def tearDownClass(cls): + frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) + frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1) + + def tearDown(self): frappe.db.rollback() def test_product_listing_in_item_group(self): @@ -87,7 +88,6 @@ class TestItemGroupProductDataEngine(unittest.TestCase): def test_item_group_with_sub_groups(self): "Test Valid Sub Item Groups in Item Group Page." - frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1) frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0) result = get_product_filter_data(query_args={ @@ -114,4 +114,45 @@ class TestItemGroupProductDataEngine(unittest.TestCase): # check if child group is fetched if shown in website self.assertIn("_Test Item Group B - 1", child_groups) - self.assertIn("_Test Item Group B - 2", child_groups) \ No newline at end of file + self.assertIn("_Test Item Group B - 2", child_groups) + + def test_item_group_page_with_descendants_included(self): + """ + Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3). + > _Test Item Group B [Level 1] + > _Test Item Group B - 1 [Level 2] + > _Test Item Group B - 1 - 1 [Level 3] + """ + frappe.get_doc({ # create Level 3 nested child group + "doctype": "Item Group", + "is_group": 1, + "item_group_name": "_Test Item Group B - 1 - 1", + "parent_item_group": "_Test Item Group B - 1" + }).insert() + + create_regular_web_item( # create an item belonging to level 3 item group + "Test Mobile F", + item_args={"item_group": "_Test Item Group B - 1 - 1"} + ) + + frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1) + + # enable 'include descendants' in Level 1 + frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1) + + result = get_product_filter_data(query_args={ + "field_filters": {}, + "attribute_filters": {}, + "start": 0, + "item_group": "_Test Item Group B" + }) + + items = result.get("items") + item_codes = [item.get("item_code") for item in items] + + # check if all sub groups' items are pulled + self.assertEqual(len(items), 6) + self.assertIn("Test Mobile A", item_codes) + self.assertIn("Test Mobile C", item_codes) + self.assertIn("Test Mobile E", item_codes) + self.assertIn("Test Mobile F", item_codes) \ No newline at end of file diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json index a090c8d76c5..50f923d87e0 100644 --- a/erpnext/setup/doctype/item_group/item_group.json +++ b/erpnext/setup/doctype/item_group/item_group.json @@ -219,6 +219,7 @@ }, { "default": "0", + "depends_on": "show_in_website", "description": "Include Website Items belonging to child Item Groups", "fieldname": "include_descendants", "fieldtype": "Check", @@ -231,7 +232,7 @@ "is_tree": 1, "links": [], "max_attachments": 3, - "modified": "2022-03-07 09:44:47.561532", + "modified": "2022-03-09 12:27:11.055782", "modified_by": "Administrator", "module": "Setup", "name": "Item Group", diff --git a/erpnext/templates/pages/non_profit/__init__.py b/erpnext/templates/pages/non_profit/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 8a2fe7a2e39c28ccb52238651b439eba17d153ab Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Mar 2022 15:42:06 +0530 Subject: [PATCH 06/36] fix: Remove tax invoice no field --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 973c8371ea2..82854ba2a6c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -9,7 +9,6 @@ "customer_section", "title", "naming_series", - "tax_invoice_number", "customer", "customer_name", "tax_id", @@ -2027,12 +2026,6 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 - }, - { - "fieldname": "tax_invoice_number", - "fieldtype": "Data", - "label": "Tax Invoice Number", - "read_only": 1 } ], "icon": "fa fa-file-text", From 9b8258479c6e71a06303d4774df5ab3a749d9de9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Mar 2022 15:43:26 +0530 Subject: [PATCH 07/36] fix: Update timestamp --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 82854ba2a6c..80b95db8868 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2038,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-03-07 16:08:53.517903", + "modified": "2022-03-08 16:08:53.517903", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 7b37a74023b088b8dcc5114b954c716ebf7f6eae Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Mar 2022 16:04:12 +0530 Subject: [PATCH 08/36] fix: Linter --- erpnext/assets/doctype/asset/asset_dashboard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py index 1833b0e7160..c81b611a418 100644 --- a/erpnext/assets/doctype/asset/asset_dashboard.py +++ b/erpnext/assets/doctype/asset/asset_dashboard.py @@ -1,5 +1,6 @@ from frappe import _ + def get_data(): return { 'non_standard_fieldnames': { From 4d8798b0ead57eb3ae3a0b27755aad60a84989b9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Mar 2022 16:41:54 +0530 Subject: [PATCH 09/36] fix: ignore non-unique swift numbers while migrating (#30132) --- .../v12_0/move_bank_account_swift_number_to_bank.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py index b3ee3404642..7ae4c42cecf 100644 --- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py +++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py @@ -5,10 +5,13 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'bank', force=1) if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'): - frappe.db.sql(""" - UPDATE `tabBank` b, `tabBank Account` ba - SET b.swift_number = ba.swift_number WHERE b.name = ba.bank - """) + try: + frappe.db.sql(""" + UPDATE `tabBank` b, `tabBank Account` ba + SET b.swift_number = ba.swift_number WHERE b.name = ba.bank + """) + except Exception as e: + frappe.log_error(e, title="Patch Migration Failed") frappe.reload_doc('accounts', 'doctype', 'bank_account') frappe.reload_doc('accounts', 'doctype', 'payment_request') From b1c8a4543d226a1b6422a6778182cc20eacfaa56 Mon Sep 17 00:00:00 2001 From: Chillar Anand Date: Wed, 9 Mar 2022 17:05:00 +0530 Subject: [PATCH 10/36] test: Added test for monthly attendance report (#29989) Co-authored-by: Rucha Mahabal --- .../test_monthly_attendance_sheet.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py new file mode 100644 index 00000000000..b196fb5b989 --- /dev/null +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -0,0 +1,45 @@ +import frappe +from dateutil.relativedelta import relativedelta + +from frappe.tests.utils import FrappeTestCase +from frappe.utils import now_datetime + +from erpnext.hr.doctype.attendance.attendance import mark_attendance +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute + + +class TestMonthlyAttendanceSheet(FrappeTestCase): + def setUp(self): + self.employee = make_employee("test_employee@example.com") + frappe.db.delete('Attendance', {'employee': self.employee}) + + def test_monthly_attendance_sheet_report(self): + now = now_datetime() + previous_month = now.month - 1 + previous_month_first = now.replace(day=1).replace(month=previous_month).date() + + company = frappe.db.get_value('Employee', self.employee, 'company') + + # mark different attendance status on first 3 days of previous month + mark_attendance(self.employee, previous_month_first, 'Absent') + mark_attendance(self.employee, previous_month_first + relativedelta(days=1), 'Present') + mark_attendance(self.employee, previous_month_first + relativedelta(days=2), 'On Leave') + + filters = frappe._dict({ + 'month': previous_month, + 'year': now.year, + 'company': company, + }) + report = execute(filters=filters) + employees = report[1][0] + datasets = report[3]['data']['datasets'] + absent = datasets[0]['values'] + present = datasets[1]['values'] + leaves = datasets[2]['values'] + + # ensure correct attendance is reflect on the report + self.assertIn(self.employee, employees) + self.assertEqual(absent[0], 1) + self.assertEqual(present[1], 1) + self.assertEqual(leaves[2], 1) From fe4b6771b5fd935ed278cf553c864a18e3356a33 Mon Sep 17 00:00:00 2001 From: Chillar Anand Date: Wed, 9 Mar 2022 17:16:05 +0530 Subject: [PATCH 11/36] refactor: Remove dead code (#30140) --- erpnext/hr/doctype/leave_application/leave_application.py | 1 - .../report/project_profitability/project_profitability.py | 2 -- erpnext/regional/report/irs_1099/irs_1099.py | 1 - erpnext/regional/report/uae_vat_201/test_uae_vat_201.py | 3 +-- erpnext/stock/report/stock_balance/stock_balance.py | 1 - 5 files changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index ef5f4bcb0ff..345d8dc3700 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -495,7 +495,6 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day number_of_days = date_diff(to_date, from_date) + .5 else: number_of_days = date_diff(to_date, from_date) + 1 - else: number_of_days = date_diff(to_date, from_date) + 1 diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 9520cd17be2..23c3b82c6dc 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -1,14 +1,12 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import frappe from frappe import _ from frappe.utils import flt def execute(filters=None): - columns, data = [], [] data = get_data(filters) columns = get_columns() charts = get_chart_data(data) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index b1a5d109621..147a59fb012 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -30,7 +30,6 @@ def execute(filters=None): if region != 'United States': return [], [] - data = [] columns = get_columns() conditions = "" if filters.supplier_group: diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 41336873ac9..464939f39e0 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -118,8 +118,7 @@ def make_customer(): "customer_type": "Company", }) customer.insert() - else: - customer = frappe.get_doc("Customer", "_Test UAE Customer") + def make_supplier(): if not frappe.db.exists("Supplier", "_Test UAE Supplier"): diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index b4f43a7fef1..24f47c19468 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -18,7 +18,6 @@ def execute(filters=None): is_reposting_item_valuation_in_progress() if not filters: filters = {} - from_date = filters.get('from_date') to_date = filters.get('to_date') if filters.get("company"): From fc42041f8fff7bd9f3b374992565bf3eccfaf43d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Mar 2022 18:01:10 +0530 Subject: [PATCH 12/36] fix(psoa): add company filter to account --- .../process_statement_of_accounts.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 088c190f451..29f2e98e779 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', { } } }); + frm.set_query("account", function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); if(frm.doc.__islocal){ frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); frm.set_value('to_date', frappe.datetime.get_today()); From f0664279130edea2b9ba1233f5fcbd0a8a001649 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Mar 2022 10:06:07 +0530 Subject: [PATCH 13/36] fix: program enrollment button labels (#30148) --- erpnext/www/lms/macros/hero.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/www/lms/macros/hero.html b/erpnext/www/lms/macros/hero.html index 95ba8f7df28..dd3c23a0145 100644 --- a/erpnext/www/lms/macros/hero.html +++ b/erpnext/www/lms/macros/hero.html @@ -39,16 +39,13 @@ frappe.call(opts).then(res => { let success_dialog = new frappe.ui.Dialog({ title: __('Success'), - primary_action_label: __('View Program Content'), + primary_action_label: __('OK'), primary_action: function() { window.location.reload(); - }, - secondary_action: function() { - window.location.reload(); } }) success_dialog.show(); - success_dialog.set_message(__('You have successfully enrolled for the program ')); + success_dialog.set_message(__('You have successfully enrolled for the program.')); }) } From 5d5ae16fe574e2b701731e1f084d652c0959b1e8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Mar 2022 11:12:33 +0530 Subject: [PATCH 14/36] fix(psoa): no such element: dict object['account'] --- .../process_statement_of_accounts.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index f8d191cc3f8..82705a9cea4 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -64,10 +64,10 @@ {{ frappe.format(row.account, {fieldtype: "Link"}) or " " }} - {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} - {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} {% endif %} From 84568ac3410f53976c76aa3e583b739df5f2a7a6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 11:20:23 +0530 Subject: [PATCH 15/36] chore: imports --- .../monthly_attendance_sheet/test_monthly_attendance_sheet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py index b196fb5b989..952af8117e2 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -1,6 +1,5 @@ import frappe from dateutil.relativedelta import relativedelta - from frappe.tests.utils import FrappeTestCase from frappe.utils import now_datetime From 5193a637810268c7730e2ce386b2c234924cc28f Mon Sep 17 00:00:00 2001 From: Himanshu Date: Thu, 10 Mar 2022 08:13:35 +0000 Subject: [PATCH 16/36] fix: do not reset asset_category (#29696) --- erpnext/stock/doctype/item/item.js | 16 ++++++++-------- erpnext/stock/doctype/item/item.py | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index ffea9c2d6e0..9e8b3bd4637 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -165,21 +165,21 @@ frappe.ui.form.on("Item", { frm.set_value('has_batch_no', 0); frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); - frm.call({ - method: "set_asset_naming_series", - doc: frm.doc, - callback: function() { + frappe.call({ + method: "erpnext.stock.doctype.item.item.get_asset_naming_series", + callback: function(r) { frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1); - frm.trigger("set_asset_naming_series"); + frm.events.set_asset_naming_series(frm, r.message); } }); frm.trigger('auto_create_assets'); }, - set_asset_naming_series: function(frm) { - if (frm.doc.__onload && frm.doc.__onload.asset_naming_series) { - frm.set_df_property("asset_naming_series", "options", frm.doc.__onload.asset_naming_series); + set_asset_naming_series: function(frm, asset_naming_series) { + if ((frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series) { + let naming_series = (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series; + frm.set_df_property("asset_naming_series", "options", naming_series); } }, diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 494fb3b8bb2..32c72fd2f64 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -50,15 +50,7 @@ class DataValidationError(frappe.ValidationError): class Item(Document): def onload(self): self.set_onload('stock_exists', self.stock_ledger_created()) - self.set_asset_naming_series() - - @frappe.whitelist() - def set_asset_naming_series(self): - if not hasattr(self, '_asset_naming_series'): - from erpnext.assets.doctype.asset.asset import get_asset_naming_series - self._asset_naming_series = get_asset_naming_series() - - self.set_onload('asset_naming_series', self._asset_naming_series) + self.set_onload('asset_naming_series', get_asset_naming_series()) def autoname(self): if frappe.db.get_default("item_naming_by") == "Naming Series": @@ -999,7 +991,7 @@ def get_uom_conv_factor(uom, stock_uom): if uom == stock_uom: return 1.0 - from_uom, to_uom = uom, stock_uom # renaming for readability + from_uom, to_uom = uom, stock_uom # renaming for readability exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1) if exact_match: @@ -1011,9 +1003,9 @@ def get_uom_conv_factor(uom, stock_uom): # This attempts to try and get conversion from intermediate UOM. # case: - # g -> mg = 1000 - # g -> kg = 0.001 - # therefore kg -> mg = 1000 / 0.001 = 1,000,000 + # g -> mg = 1000 + # g -> kg = 0.001 + # therefore kg -> mg = 1000 / 0.001 = 1,000,000 intermediate_match = frappe.db.sql(""" select (first.value / second.value) as value from `tabUOM Conversion Factor` first @@ -1072,3 +1064,11 @@ def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> Non frappe.bold(item_default.company), frappe.bold(frappe.unscrub(field)) ), title=_("Invalid Item Defaults")) + + +@frappe.whitelist() +def get_asset_naming_series(): + from erpnext.assets.doctype.asset.asset import get_asset_naming_series + + return get_asset_naming_series() + From a13e06156b3c195d2340dafcebe0f12d2c95dba8 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 10 Mar 2022 13:54:00 +0530 Subject: [PATCH 17/36] fix: 'save_quotations_as_draft' checkbox not honoured - Make sure `request_for_quotation` considers `save_quotations_as_draft` - Added test for checkout disabled quote --- erpnext/e_commerce/shopping_cart/cart.py | 6 +++- .../shopping_cart/test_shopping_cart.py | 35 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 372aed0b95f..0f3f69d18d4 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -120,7 +120,11 @@ def place_order(): def request_for_quotation(): quotation = _get_cart_quotation() quotation.flags.ignore_permissions = True - quotation.submit() + + if get_shopping_cart_settings().save_quotations_as_draft: + quotation.save() + else: + quotation.submit() return quotation.name @frappe.whitelist() diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index 9c389d0d0b4..4a3787a3133 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -6,7 +6,7 @@ import unittest import frappe from frappe.tests.utils import change_settings -from frappe.utils import add_months, nowdate +from frappe.utils import add_months, cint, nowdate from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.e_commerce.doctype.website_item.website_item import make_website_item @@ -14,22 +14,17 @@ from erpnext.e_commerce.shopping_cart.cart import ( _get_cart_quotation, get_cart_quotation, get_party, + request_for_quotation, update_cart, ) from erpnext.tests.utils import create_test_contact_and_address -# test_dependencies = ['Payment Terms Template'] class TestShoppingCart(unittest.TestCase): """ Note: Shopping Cart == Quotation """ - - @classmethod - def tearDownClass(cls): - frappe.db.sql("delete from `tabTax Rule`") - def setUp(self): frappe.set_user("Administrator") create_test_contact_and_address() @@ -45,6 +40,10 @@ class TestShoppingCart(unittest.TestCase): frappe.set_user("Administrator") self.disable_shopping_cart() + @classmethod + def tearDownClass(cls): + frappe.db.sql("delete from `tabTax Rule`") + def test_get_cart_new_user(self): self.login_as_new_user() @@ -179,6 +178,28 @@ class TestShoppingCart(unittest.TestCase): # test if items are rendered without error frappe.render_template("templates/includes/cart/cart_items.html", cart) + @change_settings("E Commerce Settings",{ + "save_quotations_as_draft": 1 + }) + def test_cart_without_checkout_and_draft_quotation(self): + "Test impact of 'save_quotations_as_draft' checkbox." + frappe.local.shopping_cart_settings = None + + # add item to cart + update_cart("_Test Item", 1) + quote_name = request_for_quotation() # Request for Quote + quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) + + self.assertEqual(quote_doctstatus, 0) + + frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0) + frappe.local.shopping_cart_settings = None + update_cart("_Test Item", 1) + quote_name = request_for_quotation() # Request for Quote + quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus")) + + self.assertEqual(quote_doctstatus, 1) + def create_tax_rule(self): tax_rule = frappe.get_test_records("Tax Rule")[0] try: From e5fb871ef4a9e738e3a5ddc6d0e8eb2dfdd2ea16 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 10 Mar 2022 13:54:43 +0530 Subject: [PATCH 18/36] fix: Ignore missing customer group while fetching price list --- erpnext/accounts/party.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b4438527158..791cd1d5e99 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -170,10 +170,7 @@ def get_default_price_list(party): return party.default_price_list if party.doctype == "Customer": - try: - return frappe.get_cached_value("Customer Group", party.customer_group, "default_price_list") - except frappe.exceptions.DoesNotExistError: - return + return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list") def set_price_list(party_details, party, party_type, given_price_list, pos=None): From 9fae89ff6105622c592b9efddb0ef4f8e0dcf942 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 10 Mar 2022 14:22:12 +0530 Subject: [PATCH 19/36] fix: flaky tests (#30154) --- .../hr/doctype/attendance/test_attendance.py | 16 +++++++++--- .../test_leave_application.py | 25 +++++++++++++------ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index c74967d213e..6095413771c 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -25,7 +25,9 @@ class TestAttendance(FrappeTestCase): self.assertEqual(attendance, fetch_attendance) def test_unmarked_days(self): - first_day = get_first_day(getdate()) + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) frappe.db.delete('Attendance', {'employee': employee}) @@ -34,7 +36,7 @@ class TestAttendance(FrappeTestCase): holiday_list = make_holiday_list() frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) - first_sunday = get_first_sunday(holiday_list) + first_sunday = get_first_sunday(holiday_list, for_date=first_day) mark_attendance(employee, first_day, 'Present') month_name = get_month_name(first_day) @@ -49,7 +51,9 @@ class TestAttendance(FrappeTestCase): self.assertIn(first_sunday, unmarked_days) def test_unmarked_days_excluding_holidays(self): - first_day = get_first_day(getdate()) + now = now_datetime() + previous_month = now.month - 1 + first_day = now.replace(day=1).replace(month=previous_month).date() employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) frappe.db.delete('Attendance', {'employee': employee}) @@ -58,7 +62,7 @@ class TestAttendance(FrappeTestCase): holiday_list = make_holiday_list() frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) - first_sunday = get_first_sunday(holiday_list) + first_sunday = get_first_sunday(holiday_list, for_date=first_day) mark_attendance(employee, first_day, 'Present') month_name = get_month_name(first_day) @@ -83,6 +87,10 @@ class TestAttendance(FrappeTestCase): relieving_date=relieving_date) frappe.db.delete('Attendance', {'employee': employee}) + from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + holiday_list = make_holiday_list() + frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) + attendance_date = add_days(first_day, 2) mark_attendance(employee, attendance_date, 'Present') month_name = get_month_name(first_day) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 7b3aa497298..01e0ca045b7 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -133,7 +133,9 @@ class TestLeaveApplication(unittest.TestCase): holiday_list = make_holiday_list() employee = get_employee() - frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) + original_holiday_list = employee.holiday_list + frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) + first_sunday = get_first_sunday(holiday_list) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) @@ -143,6 +145,8 @@ class TestLeaveApplication(unittest.TestCase): leave_application.cancel() + frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) + def test_attendance_update_for_exclude_holidays(self): # Case 2: leave type with 'Include holidays within leaves as leaves' disabled frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) @@ -157,7 +161,8 @@ class TestLeaveApplication(unittest.TestCase): holiday_list = make_holiday_list() employee = get_employee() - frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) + original_holiday_list = employee.holiday_list + frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list) first_sunday = get_first_sunday(holiday_list) # already marked attendance on a holiday should be deleted in this case @@ -177,7 +182,7 @@ class TestLeaveApplication(unittest.TestCase): attendance.flags.ignore_validate = True attendance.save() - leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company) leave_application.reload() # holiday should be excluded while marking attendance self.assertEqual(leave_application.total_leave_days, 3) @@ -189,6 +194,8 @@ class TestLeaveApplication(unittest.TestCase): # attendance on non-holiday updated self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave") + frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) + def test_block_list(self): self._clear_roles() @@ -327,7 +334,8 @@ class TestLeaveApplication(unittest.TestCase): employee = get_employee() default_holiday_list = make_holiday_list() - frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list) + original_holiday_list = employee.holiday_list + frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list) first_sunday = get_first_sunday(default_holiday_list) optional_leave_date = add_days(first_sunday, 1) @@ -378,6 +386,8 @@ class TestLeaveApplication(unittest.TestCase): # check leave balance is reduced self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9) + frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) + def test_leaves_allowed(self): employee = get_employee() leave_period = get_leave_period() @@ -782,9 +792,10 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el allocate_leave.submit() -def get_first_sunday(holiday_list): - month_start_date = get_first_day(nowdate()) - month_end_date = get_last_day(nowdate()) +def get_first_sunday(holiday_list, for_date=None): + date = for_date or getdate() + month_start_date = get_first_day(date) + month_end_date = get_last_day(date) first_sunday = frappe.db.sql(""" select holiday_date from `tabHoliday` where parent = %s From 6794148c04d6cfdb3ac9b4df38e0957f35334c5a Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 10 Mar 2022 10:17:29 +0100 Subject: [PATCH 20/36] fix(lead): reload contact before updating links (#29966) * fix(lead): reload contact before updading links Contact might have changed since it was created. * refactor: reload contact after insert --- erpnext/crm/doctype/lead/lead.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index c31b068a43f..33ec5521522 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -214,6 +214,7 @@ class Lead(SellingController): }) contact.insert(ignore_permissions=True) + contact.reload() # load changes by hooks on contact return contact From 412645597567df133dd91a6cf44db93207575052 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 15:43:09 +0530 Subject: [PATCH 21/36] fix: dont reset UOM in MR on every get_item_detail call (#30164) --- erpnext/stock/doctype/material_request/material_request.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 5f53be0869e..e68b0abfb94 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -214,6 +214,7 @@ frappe.ui.form.on('Material Request', { material_request_type: frm.doc.material_request_type, plc_conversion_rate: 1, rate: item.rate, + uom: item.uom, conversion_factor: item.conversion_factor }, overwrite_warehouse: overwrite_warehouse @@ -392,6 +393,7 @@ frappe.ui.form.on("Material Request Item", { item_code: function(frm, doctype, name) { const item = locals[doctype][name]; item.rate = 0; + item.uom = ''; set_schedule_date(frm); frm.events.get_item_data(frm, item, true); }, From b4d4ae6aa3f258357a974e0a76247b3e752bdbf2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 16:09:26 +0530 Subject: [PATCH 22/36] test: refactor item merge test and disable commits --- erpnext/stock/doctype/item/test_item.py | 19 ++++++++++--------- .../repost_item_valuation.py | 3 ++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index d7671b1d714..d57308b2afa 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -371,23 +371,24 @@ class TestItem(FrappeTestCase): variant.save() def test_item_merging(self): - create_item("Test Item for Merging 1") - create_item("Test Item for Merging 2") + old = create_item(frappe.generate_hash(length=20)).name + new = create_item(frappe.generate_hash(length=20)).name - make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC", + make_stock_entry(item_code=old, target="_Test Warehouse - _TC", qty=1, rate=100) - make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC", + make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", + qty=1, rate=100) + make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", qty=1, rate=100) - frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True) + frappe.rename_doc("Item", old, new, merge=True) - self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1")) + self.assertFalse(frappe.db.exists("Item", old)) self.assertTrue(frappe.db.get_value("Bin", - {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"})) - + {"item_code": new, "warehouse": "_Test Warehouse - _TC"})) self.assertTrue(frappe.db.get_value("Bin", - {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"})) def test_item_merging_with_product_bundle(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle 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 977d470995e..f4d52ad73d1 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -118,7 +118,8 @@ def repost(doc): doc.set_status('Failed') raise finally: - frappe.db.commit() + if not frappe.flags.in_test: + frappe.db.commit() def repost_sl_entries(doc): if doc.based_on == 'Transaction': From 73901aad6f88c06cfb6dab8da133fba4175bd692 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 16:30:29 +0530 Subject: [PATCH 23/36] fix: handle duplicate bins during item merge renames --- erpnext/stock/doctype/item/item.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 32c72fd2f64..8ede95539b3 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -392,6 +392,7 @@ class Item(Document): self.validate_properties_before_merge(new_name) self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name) + self.delete_old_bins(old_name) def after_rename(self, old_name, new_name, merge): if merge: @@ -420,6 +421,9 @@ class Item(Document): frappe.db.set_value(dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False) + def delete_old_bins(self, old_name): + frappe.db.delete("Bin", {"item_code": old_name}) + def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name): records = frappe.db.sql(""" SELECT parent, COUNT(*) as records FROM `tabStock Reconciliation Item` @@ -500,11 +504,11 @@ class Item(Document): existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse - from tabBin where item_code=%s""", new_name) + repost_stock_for_warehouses = frappe.get_all("Stock Ledger Entry", + "warehouse", filters={"item_code": new_name}, pluck="warehouse", distinct=True) # Delete all existing bins to avoid duplicate bins for the same item and warehouse - frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) + frappe.db.delete("Bin", {"item_code": new_name}) for warehouse in repost_stock_for_warehouses: repost_stock(new_name, warehouse) From 18e2a33a9be4d4efea13ce5711343413b31358b8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 16:44:05 +0530 Subject: [PATCH 24/36] fix: fetch new fields in bom from routing --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 37d2b9ff978..c0fb63f8280 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -192,7 +192,8 @@ class BOM(WebsiteGenerator): if self.routing: self.set("operations", []) fields = ["sequence_id", "operation", "workstation", "description", - "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"] + "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate", + "set_cost_based_on_bom_qty", "fixed_time"] for row in frappe.get_all("BOM Operation", fields = fields, filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"): From 362102e802bb501312a39bb21810336de26696b9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 16:49:29 +0530 Subject: [PATCH 25/36] fix: dont hardcode hour rate precision --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- .../manufacturing/doctype/bom_operation/bom_operation.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c0fb63f8280..a8ce1d7642c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -198,7 +198,7 @@ class BOM(WebsiteGenerator): for row in frappe.get_all("BOM Operation", fields = fields, filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"): child = self.append('operations', row) - child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2) + child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate")) def set_bom_material_details(self): for item in self.get("items"): diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index c7be7efc9e1..341f9692c45 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -66,7 +66,8 @@ "label": "Hour Rate", "oldfieldname": "hour_rate", "oldfieldtype": "Currency", - "options": "currency" + "options": "currency", + "precision": "2" }, { "description": "In minutes", @@ -186,7 +187,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-12-15 03:00:00.473173", + "modified": "2022-03-10 06:19:08.462027", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", From 7dd10367f49b9f67def80aa0daa612848f00092c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 17:07:57 +0530 Subject: [PATCH 26/36] fix: only update valuation rate if not None --- erpnext/stock/stock_ledger.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ba1081f4dce..353bfa452b6 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -838,11 +838,13 @@ class update_entries_after(object): for warehouse, data in self.data.items(): bin_name = get_or_make_bin(self.item_code, warehouse) - frappe.db.set_value('Bin', bin_name, { - "valuation_rate": data.valuation_rate, + updated_values = { "actual_qty": data.qty_after_transaction, "stock_value": data.stock_value - }) + } + if data.valuation_rate is not None: + updated_values["valuation_rate"] = data.valuation_rate + frappe.db.set_value('Bin', bin_name, updated_values) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): From 6c54be0dcd83cc021844017f2ceeed58a88ca920 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 17:37:29 +0530 Subject: [PATCH 27/36] test: flaky MR report test all test records are on same day so sorting was random in report rows. --- ...test_requested_items_to_order_and_receive.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py index f3c751c5c3c..a533da00e3a 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py @@ -17,8 +17,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase): def setUp(self) -> None: create_item("Test MR Report Item") self.setup_material_request() # to order and receive - self.setup_material_request(order=True) # to receive (ordered) - self.setup_material_request(order=True, receive=True) # complete (ordered & received) + self.setup_material_request(order=True, days=1) # to receive (ordered) + self.setup_material_request(order=True, receive=True, days=2) # complete (ordered & received) self.filters = frappe._dict( company="_Test Company", from_date=today(), to_date=add_days(today(), 30), @@ -32,9 +32,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase): data = get_data(self.filters) self.assertEqual(len(data), 2) # MRs today should be fetched - self.filters.from_date = add_days(today(), 1) - data = get_data(self.filters) - self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow + data = get_data(self.filters.update({"from_date": add_days(today(), 10)})) + self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is in future def test_ordered_received_material_requests(self): data = get_data(self.filters) @@ -44,19 +43,19 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase): self.assertEqual(data[0].ordered_qty, 0.0) self.assertEqual(data[1].ordered_qty, 57.0) - def setup_material_request(self, order=False, receive=False): + def setup_material_request(self, order=False, receive=False, days=0): po = None test_records = frappe.get_test_records('Material Request') mr = frappe.copy_doc(test_records[0]) - mr.transaction_date = today() - mr.schedule_date = add_days(today(), 1) + mr.transaction_date = add_days(today(), days) + mr.schedule_date = add_days(mr.transaction_date, 1) for row in mr.items: row.item_code = "Test MR Report Item" row.item_name = "Test MR Report Item" row.description = "Test MR Report Item" row.uom = "Nos" - row.schedule_date = add_days(today(), 1) + row.schedule_date = mr.schedule_date mr.submit() if order or receive: From 472fe8e3191764d281b54c0c49c83a88b26deed9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Mar 2022 17:39:55 +0530 Subject: [PATCH 28/36] test: submit PR directly --- .../stock/doctype/purchase_receipt/test_purchase_receipt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index fa28f2252de..0017fa7ee11 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -769,8 +769,7 @@ class TestPurchaseReceipt(FrappeTestCase): update_purchase_receipt_status, ) - pr = make_purchase_receipt(do_not_submit=True) - pr.submit() + pr = make_purchase_receipt() update_purchase_receipt_status(pr.name, "Closed") self.assertEqual( From d596e0e4dff86cf8cde640bd18a54ee159276a4c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Mar 2022 20:56:36 +0530 Subject: [PATCH 29/36] fix: Shipping rule application fixes --- .../doctype/shipping_rule/shipping_rule.py | 3 +-- erpnext/controllers/taxes_and_totals.py | 5 ++++- .../public/js/controllers/taxes_and_totals.js | 4 +++- .../doctype/sales_order/test_sales_order.py | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 792e7d21a78..7e5129911e4 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -71,8 +71,7 @@ class ShippingRule(Document): if doc.currency != doc.company_currency: shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) - if shipping_amount: - self.add_shipping_rule_to_tax_table(doc, shipping_amount) + self.add_shipping_rule_to_tax_table(doc, shipping_amount) def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index a1bb6670c42..d362cdde110 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object): self.set_discount_amount() self.apply_discount_amount() + self.calculate_shipping_charges() + if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: self.calculate_total_advance() @@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object): self.initialize_taxes() self.determine_exclusive_rate() self.calculate_net_total() - self.calculate_shipping_charges() self.calculate_taxes() self.manipulate_grand_total_for_inclusive_tax() self.calculate_totals() @@ -276,6 +277,8 @@ class calculate_taxes_and_totals(object): shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule.apply(self.doc) + self._calculate() + def calculate_taxes(self): rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment') if not rounding_adjustment_computed: diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index ae0e2a3f6f8..9fec43b74ef 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -39,6 +39,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this._calculate_taxes_and_totals(); this.calculate_discount_amount(); + this.calculate_shipping_charges(); + // Advance calculation applicable to Sales /Purchase Invoice if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype) && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) { @@ -81,7 +83,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.initialize_taxes(); this.determine_exclusive_rate(); this.calculate_net_total(); - this.calculate_shipping_charges(); this.calculate_taxes(); this.manipulate_grand_total_for_inclusive_tax(); this.calculate_totals(); @@ -275,6 +276,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { this.shipping_rule(); + this._calculate_taxes_and_totals(); } } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index b5284793e13..86a08828b26 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1477,6 +1477,28 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) self.assertEqual(mr.status, "Manufactured") + def test_sales_order_with_shipping_rule(self): + from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule + shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test") + sales_order = make_sales_order(do_not_save=True) + sales_order.shipping_rule = shipping_rule.name + + sales_order.items[0].qty = 1 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 50) + + sales_order.items[0].qty = 2 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 100) + + sales_order.items[0].qty = 3 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 200) + + sales_order.items[0].qty = 21 + sales_order.save() + self.assertEqual(sales_order.taxes[0].tax_amount, 0) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable From d9c91748f4e8eb37ec1c76ee5718c38de8ce9f65 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 15 Feb 2022 16:35:04 +0530 Subject: [PATCH 30/36] fix: if an item code is too long, truncate before setting BOM name --- erpnext/manufacturing/doctype/bom/bom.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a8ce1d7642c..3c325037e52 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -121,7 +121,21 @@ class BOM(WebsiteGenerator): else: idx = 1 - name = 'BOM-' + self.item + ('-%.3i' % idx) + prefix = self.doctype + suffix = "%.3i" % idx + bom_name = prefix + "-" + self.item + "-" + suffix + + if len(bom_name) <= 140: + name = bom_name + else: + # since max characters for name is 140, remove enough characters from the + # item name to fit the prefix, suffix and the separators + truncated_length = 140 - (len(prefix) + len(suffix) + 2) + truncated_item_name = self.item[:truncated_length] + # if a partial word is found after truncate, remove the extra characters + truncated_item_name = truncated_item_name.rsplit(" ", 1)[0] + name = prefix + "-" + truncated_item_name + "-" + suffix + if frappe.db.exists("BOM", name): conflicting_bom = frappe.get_doc("BOM", name) From e2c99e02a95a87021786a0666e97e174a3f65a44 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 16 Feb 2022 12:35:34 +0530 Subject: [PATCH 31/36] test: bom for item_code that is >VARCHAR_LEN --- erpnext/manufacturing/doctype/bom/test_bom.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 3cc91b341c6..e472388d368 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -432,6 +432,15 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.transfer_material_against, "Work Order") bom.delete() + def test_bom_name_length(self): + """ test >140 char names""" + bom_tree = { + "x" * 140 : { + " ".join(["abc"] * 35): {} + } + } + create_nested_bom(bom_tree, prefix="") + 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 7f2670941ca7f2e41a37a8ea3ed186a0fa04b57c Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 16 Feb 2022 15:55:25 +0530 Subject: [PATCH 32/36] fix: improve bom autoname logic --- erpnext/manufacturing/doctype/bom/bom.py | 38 ++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3c325037e52..2c342c0d691 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import functools +import re from collections import deque from operator import itemgetter from typing import List @@ -103,27 +104,34 @@ class BOM(WebsiteGenerator): ) def autoname(self): - names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item) + existing_boms = frappe.get_all("BOM", filters={"item": self.item}) + if existing_boms: + existing_bom_names = [bom.name for bom in existing_boms] - if names: - # name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1 + # split by "/" and "-" + delimiters = ["/", "-"] + pattern = "|".join(map(re.escape, delimiters)) + bom_parts = [re.split(pattern, bom_name) for bom_name in existing_bom_names] - # split by item - names = [name.split(self.item, 1) for name in names] - names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)] + # filter out BOMs that do not follow the following formats: + # - BOM/ITEM/001 + # - BOM/ITEM/001-1 + # - BOM-ITEM-001 + # - BOM-ITEM-001-1 + valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) - # split by (-) if cancelled - if names: - names = [cint(name.split('-')[-1]) for name in names] - idx = max(names) + 1 + # extract the current index from the BOM parts + if valid_bom_parts: + indexes = [cint(part[-1]) for part in valid_bom_parts] + index = max(indexes) + 1 else: - idx = 1 + index = 1 else: - idx = 1 + index = 1 prefix = self.doctype - suffix = "%.3i" % idx - bom_name = prefix + "-" + self.item + "-" + suffix + suffix = "%.3i" % index # convert index to string (1 -> "001") + bom_name = f"{prefix}-{self.item}-{suffix}" if len(bom_name) <= 140: name = bom_name @@ -134,7 +142,7 @@ class BOM(WebsiteGenerator): truncated_item_name = self.item[:truncated_length] # if a partial word is found after truncate, remove the extra characters truncated_item_name = truncated_item_name.rsplit(" ", 1)[0] - name = prefix + "-" + truncated_item_name + "-" + suffix + name = f"{prefix}-{truncated_item_name}-{suffix}" if frappe.db.exists("BOM", name): conflicting_bom = frappe.get_doc("BOM", name) From 6b58d534030df956f9983c003741ad69263aa287 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 26 Feb 2022 11:36:44 +0530 Subject: [PATCH 33/36] refactor: split versioning code for testability --- erpnext/manufacturing/doctype/bom/bom.py | 46 ++++++++++--------- erpnext/manufacturing/doctype/bom/test_bom.py | 21 +++++++++ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 2c342c0d691..817b8e986e8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -104,28 +104,9 @@ class BOM(WebsiteGenerator): ) def autoname(self): - existing_boms = frappe.get_all("BOM", filters={"item": self.item}) + existing_boms = frappe.get_all("BOM", filters={"item": self.item}, pluck="name") if existing_boms: - existing_bom_names = [bom.name for bom in existing_boms] - - # split by "/" and "-" - delimiters = ["/", "-"] - pattern = "|".join(map(re.escape, delimiters)) - bom_parts = [re.split(pattern, bom_name) for bom_name in existing_bom_names] - - # filter out BOMs that do not follow the following formats: - # - BOM/ITEM/001 - # - BOM/ITEM/001-1 - # - BOM-ITEM-001 - # - BOM-ITEM-001-1 - valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) - - # extract the current index from the BOM parts - if valid_bom_parts: - indexes = [cint(part[-1]) for part in valid_bom_parts] - index = max(indexes) + 1 - else: - index = 1 + index = self.get_next_version_index(existing_boms) else: index = 1 @@ -156,6 +137,29 @@ class BOM(WebsiteGenerator): self.name = name + @staticmethod + def get_next_version_index(existing_boms: List[str]) -> int: + # split by "/" and "-" + delimiters = ["/", "-"] + pattern = "|".join(map(re.escape, delimiters)) + bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms] + + # filter out BOMs that do not follow the following formats: + # - BOM/ITEM/001 + # - BOM/ITEM/001-1 + # - BOM-ITEM-001 + # - BOM-ITEM-001-1 + valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) + + # extract the current index from the BOM parts + if valid_bom_parts: + indexes = [cint(part[-1]) for part in valid_bom_parts] + index = max(indexes) + 1 + else: + index = 1 + + return index + def validate(self): self.route = frappe.scrub(self.name).replace('_', '-') diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index e472388d368..346870d1d1c 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -441,6 +441,27 @@ class TestBOM(FrappeTestCase): } create_nested_bom(bom_tree, prefix="") + def test_version_index(self): + + bom = frappe.new_doc("BOM") + + version_index_test_cases = [ + (1, []), + (1, ["BOM#XYZ"]), + (2, ["BOM/ITEM/001"]), + (2, ["BOM/ITEM/001", "BOM/ITEM/001-1"]), + (2, ["BOM-ITEM-001",]), + (2, ["BOM-ITEM-001", "BOM-ITEM-001-1"]), + (3, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-001-1"]), + (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]), + (2, ["BOM-ITEM-001", "BOM-ITEM-001-1", "BOM-ITEM-001-2"]), + ] + + for expected_index, existing_boms in version_index_test_cases: + with self.subTest(): + self.assertEqual(expected_index, bom.get_next_version_index(existing_boms), + msg=f"Incorrect index for {existing_boms}") + 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 67d8a7ba86c3131b19b4077055524f69d334314e Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 2 Mar 2022 15:25:06 +0530 Subject: [PATCH 34/36] fix: cancelled document check --- erpnext/manufacturing/doctype/bom/bom.py | 15 +++++++++------ erpnext/manufacturing/doctype/bom/test_bom.py | 7 ++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 817b8e986e8..a025ff740c6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -104,7 +104,13 @@ class BOM(WebsiteGenerator): ) def autoname(self): - existing_boms = frappe.get_all("BOM", filters={"item": self.item}, pluck="name") + # ignore amended documents while calculating current index + existing_boms = frappe.get_all( + "BOM", + filters={"item": self.item, "amended_from": ["is", "not set"]}, + pluck="name" + ) + if existing_boms: index = self.get_next_version_index(existing_boms) else: @@ -144,15 +150,12 @@ class BOM(WebsiteGenerator): pattern = "|".join(map(re.escape, delimiters)) bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms] - # filter out BOMs that do not follow the following formats: - # - BOM/ITEM/001 - # - BOM/ITEM/001-1 - # - BOM-ITEM-001 - # - BOM-ITEM-001-1 + # filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001 valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts)) # extract the current index from the BOM parts if valid_bom_parts: + # handle cancelled and submitted documents indexes = [cint(part[-1]) for part in valid_bom_parts] index = max(indexes) + 1 else: diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 346870d1d1c..6f9dff454e2 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -449,12 +449,9 @@ class TestBOM(FrappeTestCase): (1, []), (1, ["BOM#XYZ"]), (2, ["BOM/ITEM/001"]), - (2, ["BOM/ITEM/001", "BOM/ITEM/001-1"]), - (2, ["BOM-ITEM-001",]), - (2, ["BOM-ITEM-001", "BOM-ITEM-001-1"]), - (3, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-001-1"]), + (2, ["BOM-ITEM-001"]), + (3, ["BOM-ITEM-001", "BOM-ITEM-002"]), (4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]), - (2, ["BOM-ITEM-001", "BOM-ITEM-001-1", "BOM-ITEM-001-2"]), ] for expected_index, existing_boms in version_index_test_cases: From 94d0f8d4e79aa87d66f73bc7056cf5faf9114588 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 11 Mar 2022 16:48:53 +0530 Subject: [PATCH 35/36] test: actual bom naming test --- erpnext/manufacturing/doctype/bom/test_bom.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 6f9dff454e2..4417123178c 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -459,6 +459,42 @@ class TestBOM(FrappeTestCase): self.assertEqual(expected_index, bom.get_next_version_index(existing_boms), msg=f"Incorrect index for {existing_boms}") + def test_bom_versioning(self): + bom_tree = { + frappe.generate_hash(length=10) : { + frappe.generate_hash(length=10): {} + } + } + bom = create_nested_bom(bom_tree, prefix="") + self.assertEqual(int(bom.name.split("-")[-1]), 1) + original_bom_name = bom.name + + bom.cancel() + bom.reload() + self.assertEqual(bom.name, original_bom_name) + + # create a new amendment + amendment = frappe.copy_doc(bom) + amendment.docstatus = 0 + amendment.amended_from = bom.name + + amendment.save() + amendment.submit() + amendment.reload() + + self.assertNotEqual(amendment.name, bom.name) + # `origname-001-1` version + self.assertEqual(int(amendment.name.split("-")[-1]), 1) + self.assertEqual(int(amendment.name.split("-")[-2]), 1) + + # create a new version + version = frappe.copy_doc(amendment) + version.docstatus = 0 + version.amended_from = None + version.save() + self.assertNotEqual(amendment.name, version.name) + self.assertEqual(int(version.name.split("-")[-1]), 2) + 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 b085e96a1265aebddb895ea4a5e8df6b5f9e72bc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 11 Mar 2022 18:28:50 +0530 Subject: [PATCH 36/36] revert: "Merge pull request #29290 from s-aga-r/fix/delivery-note/billed-amount" (#29782) (#29807) * Revert "Merge pull request #29290 from s-aga-r/fix/delivery-note/billed-amount" This reverts commit 038f94955006c88209f9df28e3a785c59a4ddb28, reversing changes made to c7b491843476bca89be02851ccafb7e409876609. * fix: linter (cherry picked from commit 7fa46f77e0bdbc516b3c0cb0fb20594ee7fa398b) # Conflicts: # erpnext/patches.txt Co-authored-by: Sagar Sharma --- .../doctype/sales_invoice/sales_invoice.py | 6 +++--- .../stock/doctype/delivery_note/delivery_note.py | 16 ++++------------ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 573da276a2a..862ac81ff38 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1255,14 +1255,14 @@ class SalesInvoice(SellingController): def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] for d in self.get("items"): - if d.so_detail: - updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) - elif d.dn_detail: + if d.dn_detail: billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` where dn_detail=%s and docstatus=1""", d.dn_detail) billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) updated_delivery_notes.append(d.delivery_note) + elif d.so_detail: + updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) for dn in set(updated_delivery_notes): frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 2a4d63954a7..fbcc8038aa0 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -342,25 +342,21 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum # Billed against Sales Order directly - si = frappe.qb.DocType("Sales Invoice").as_("si") si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") sum_amount = Sum(si_item.amount).as_("amount") - billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where( - (si_item.parent == si.name) & + billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where( (si_item.so_detail == so_detail) & ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & - (si_item.docstatus == 1) & - (si.update_stock == 0) + (si_item.docstatus == 1) ).run() billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row - dn = frappe.qb.DocType("Delivery Note").as_("dn") dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") - dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where( + dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where( (dn.name == dn_item.parent) & (dn_item.so_detail == so_detail) & (dn.docstatus == 1) & @@ -385,11 +381,7 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): # Distribute billed amount directly against SO between DNs based on FIFO if billed_against_so and billed_amt_agianst_dn < dnd.amount: - if dnd.returned_qty: - pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty - else: - pending_to_bill = flt(dnd.amount) - pending_to_bill -= billed_amt_agianst_dn + pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn if pending_to_bill <= billed_against_so: billed_amt_agianst_dn += pending_to_bill billed_against_so -= pending_to_bill