From a4043c035d4116e206e486d4eecabe897cf24630 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:27:06 +0530 Subject: [PATCH 01/28] fix: Prevent clearing of Depreciation Schedule on adding more than one Finance Book --- 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 c0c437f8d2..7031ed15bd 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -185,11 +185,11 @@ class Asset(AccountsController): if not self.available_for_use_date: return + start = self.clear_depreciation_schedule() + for d in self.get('finance_books'): self.validate_asset_finance_books(d) - start = self.clear_depreciation_schedule() - # value_after_depreciation - current Asset value if self.docstatus == 1 and d.value_after_depreciation: value_after_depreciation = (flt(d.value_after_depreciation) - From 059d1f3b74b2bee707387cea518eeb394bfce3d8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:35:41 +0530 Subject: [PATCH 02/28] fix: Rename loop variable --- erpnext/assets/doctype/asset/asset.py | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7031ed15bd..c00d6836a6 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -187,23 +187,23 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() - for d in self.get('finance_books'): - self.validate_asset_finance_books(d) + for finance_book in self.get('finance_books'): + self.validate_asset_finance_books(finance_book) # value_after_depreciation - current Asset value - if self.docstatus == 1 and d.value_after_depreciation: - value_after_depreciation = (flt(d.value_after_depreciation) - + if self.docstatus == 1 and finance_book.value_after_depreciation: + value_after_depreciation = (flt(finance_book.value_after_depreciation) - flt(self.opening_accumulated_depreciation)) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) - d.value_after_depreciation = value_after_depreciation + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ cint(self.number_of_depreciations_booked) - has_pro_rata = self.check_is_pro_rata(d) + has_pro_rata = self.check_is_pro_rata(finance_book) if has_pro_rata: number_of_pending_depreciations += 1 @@ -213,56 +213,56 @@ class Asset(AccountsController): # If depreciation is already completed (for double declining balance) if skip_row: continue - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d) + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(d.depreciation_start_date, - n * cint(d.frequency_of_depreciation)) + schedule_date = add_months(finance_book.depreciation_start_date, + n * cint(finance_book.frequency_of_depreciation)) # schedule date will be a year later from start date # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) + monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) # if asset is being sold if date_of_sale: - from_date = self.get_from_date(d.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, + from_date = self.get_from_date(finance_book.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, from_date, date_of_sale) if depreciation_amount > 0: self.append("schedules", { "schedule_date": date_of_sale, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) break # For first row if has_pro_rata and n==0: - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, - self.available_for_use_date, d.depreciation_start_date) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + self.available_for_use_date, finance_book.depreciation_start_date) # For first depr schedule date will be the start date # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) + monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) # 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)) + n * cint(finance_book.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount - depreciation_amount, days, months = self.get_pro_rata_amt(d, + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, schedule_date, self.to_date) depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, d.finance_book) + depreciation_amount, finance_book.finance_book) monthly_schedule_date = add_months(schedule_date, 1) schedule_date = add_days(schedule_date, days) @@ -273,10 +273,10 @@ class Asset(AccountsController): self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != d.expected_value_after_useful_life) - or value_after_depreciation < d.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life) + if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life) + or value_after_depreciation < finance_book.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) skip_row = True if depreciation_amount > 0: @@ -286,7 +286,7 @@ class Asset(AccountsController): # In pro rata case, for first and last depreciation, month range would be different month_range = months \ if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ - else d.frequency_of_depreciation + else finance_book.frequency_of_depreciation for r in range(month_range): if (has_pro_rata and n == 0): @@ -312,17 +312,17 @@ class Asset(AccountsController): self.append("schedules", { "schedule_date": date, "depreciation_amount": amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) else: self.append("schedules", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx }) # used when depreciation schedule needs to be modified due to increase in asset life From 475d8394e413d530ec57d9c7f713fecd706b2271 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 22 Nov 2021 21:23:27 +0530 Subject: [PATCH 03/28] fix: Clear Depreciation Schedule entries that aren't linked with Journal Entries before modifying the schedule --- erpnext/assets/doctype/asset/asset.py | 42 ++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c00d6836a6..f3825ea682 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -209,7 +209,8 @@ class Asset(AccountsController): number_of_pending_depreciations += 1 skip_row = False - for n in range(start, number_of_pending_depreciations): + + for n in range(start[finance_book.idx-1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -325,14 +326,39 @@ class Asset(AccountsController): "finance_book_id": finance_book.idx }) - # used when depreciation schedule needs to be modified due to increase in asset life + # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales + # JE: Journal Entry, FB: Finance Book 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 + start = [] + num_of_depreciations_completed = 0 + depr_schedule = [] + + for schedule in self.get('schedules'): + + # to ensure that start will only be updated once for each FB + if len(start) == (int(schedule.finance_book_id) - 1): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + elif len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + + # to update start when all the schedule rows corresponding to the last FB are linked with JEs + if len(start) == (len(self.finance_books) - 1): + start.append(num_of_depreciations_completed) + + # when the Depreciation Schedule is being created for the first time + if start == []: + start = [0] * len(self.finance_books) + else: + self.schedules = depr_schedule + return start def get_from_date(self, finance_book): From f455de2924f7c7554b9eefc757c354992901136b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 23:26:32 +0530 Subject: [PATCH 04/28] fix: Test if multiple Depreciation Schedules are set up for multiple Finance Books --- erpnext/assets/doctype/asset/test_asset.py | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d1d4527ec7..fd4d9ff239 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,38 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.save() + + self.assertEqual(len(asset.schedules), 9) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, 1) + else: + self.assertEqual(schedule.finance_book_id, 2) + def test_depreciation_entry_cancellation(self): asset = create_asset( item_code = "Macbook Pro", From 33a0b1db2ced1fe84386dbd6b1e5eacb535e5098 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 02:45:17 +0530 Subject: [PATCH 05/28] fix: Retain depreciation schedule rows that are linked with JEs while clearing the schedule --- erpnext/assets/doctype/asset/asset.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f3825ea682..351556c76c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -335,6 +335,11 @@ class Asset(AccountsController): for schedule in self.get('schedules'): + # to update start when there are JEs linked with all the schedule rows corresponding to an FB + if len(start) == (int(schedule.finance_book_id) - 2): + start.append(num_of_depreciations_completed) + num_of_depreciations_completed = 0 + # to ensure that start will only be updated once for each FB if len(start) == (int(schedule.finance_book_id) - 1): if schedule.journal_entry: @@ -344,11 +349,6 @@ class Asset(AccountsController): start.append(num_of_depreciations_completed) num_of_depreciations_completed = 0 - # to update start when there are JEs linked with all the schedule rows corresponding to an FB - elif len(start) == (int(schedule.finance_book_id) - 2): - start.append(num_of_depreciations_completed) - num_of_depreciations_completed = 0 - # to update start when all the schedule rows corresponding to the last FB are linked with JEs if len(start) == (len(self.finance_books) - 1): start.append(num_of_depreciations_completed) From 6ec5a190631b563804748fe43532b70d0f230ff9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 03:19:36 +0530 Subject: [PATCH 06/28] fix: Test if clear_depreciation_schedule() works for multiple finance books --- erpnext/assets/doctype/asset/test_asset.py | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fd4d9ff239..c1a978572e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -890,6 +890,50 @@ class TestDepreciationBasics(AssetSetup): self.assertEqual(len(asset.schedules), 1) + def test_clear_depreciation_schedule_for_multiple_finance_books(self): + asset = create_asset( + item_code = "Macbook Pro", + available_for_use_date = "2019-12-31", + do_not_save = 1 + ) + + asset.calculate_depreciation = 1 + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 6, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2020-12-31" + }) + asset.append("finance_books", { + "depreciation_method": "Straight Line", + "frequency_of_depreciation": 12, + "total_number_of_depreciations": 3, + "expected_value_after_useful_life": 10000, + "depreciation_start_date": "2023-12-31" + }) + asset.submit() + + post_depreciation_entries(date="2023-01-01") + asset.load_from_db() + + asset.clear_depreciation_schedule() + + self.assertEqual(len(asset.schedules), 6) + + for schedule in asset.schedules: + if schedule.idx <= 3: + self.assertEqual(schedule.finance_book_id, "1") + else: + self.assertEqual(schedule.finance_book_id, "2") + def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): asset = create_asset( item_code = "Macbook Pro", From 4311936b7dd2bf7e3f5e11fe5ce6eeff03a7d49b Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 05:21:31 +0530 Subject: [PATCH 07/28] fix: Edit dates and frequency of depreciation --- erpnext/assets/doctype/asset/test_asset.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index c1a978572e..a297a5a501 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -900,28 +900,28 @@ class TestDepreciationBasics(AssetSetup): asset.calculate_depreciation = 1 asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", - "frequency_of_depreciation": 12, + "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2020-12-31" + "depreciation_start_date": "2020-01-31" }) asset.append("finance_books", { "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, "expected_value_after_useful_life": 10000, - "depreciation_start_date": "2023-12-31" + "depreciation_start_date": "2020-12-31" }) asset.submit() - post_depreciation_entries(date="2023-01-01") + post_depreciation_entries(date="2020-04-01") asset.load_from_db() asset.clear_depreciation_schedule() From 6f1cf94c9fb2c7f7abda9802c4868b427a339ed9 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:18:30 +0530 Subject: [PATCH 08/28] fix: Fix 'Adjust Asset Value' button --- 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 c0c437f8d2..a053c7fab9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,7 +724,7 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.get_doc("Asset Value Adjustment") + asset_maintenance = frappe.new_doc("Asset Value Adjustment") asset_maintenance.update({ "asset": asset, "company": company, From 5b224f841b0419d0250f02549300e0754fa816b5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:19:35 +0530 Subject: [PATCH 09/28] fix: Rename variable --- erpnext/assets/doctype/asset/asset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a053c7fab9..ff5964be68 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -724,13 +724,13 @@ def create_asset_repair(asset, asset_name): @frappe.whitelist() def create_asset_adjustment(asset, asset_category, company): - asset_maintenance = frappe.new_doc("Asset Value Adjustment") - asset_maintenance.update({ + asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") + asset_value_adjustment.update({ "asset": asset, "company": company, "asset_category": asset_category }) - return asset_maintenance + return asset_value_adjustment @frappe.whitelist() def transfer_asset(args): From 4629308d94b4fc2a667a7eb7d526b2a810bb486a Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Thu, 2 Dec 2021 01:22:18 +0530 Subject: [PATCH 10/28] fix: Rename function --- erpnext/assets/doctype/asset/asset.js | 6 +++--- erpnext/assets/doctype/asset/asset.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index da5778ea3d..9fa50a9ade 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', { if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { - frm.trigger("create_asset_adjustment"); + frm.trigger("create_asset_value_adjustment"); }, __("Manage")); } @@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', { }); }, - create_asset_adjustment: function(frm) { + create_asset_value_adjustment: function(frm) { frappe.call({ args: { "asset": frm.doc.name, "asset_category": frm.doc.asset_category, "company": frm.doc.company }, - method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment", + method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment", freeze: 1, callback: function(r) { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ff5964be68..f2b02cb9c5 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -723,7 +723,7 @@ def create_asset_repair(asset, asset_name): return asset_repair @frappe.whitelist() -def create_asset_adjustment(asset, asset_category, company): +def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") asset_value_adjustment.update({ "asset": asset, From 14d03a94460d0c7b7e05e1c2a580e0b27cade2eb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 12 Dec 2021 12:55:30 +0530 Subject: [PATCH 11/28] fix: KSA QR field rename patch --- erpnext/patches/v13_0/rename_ksa_qr_field.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py index 0bb86e0450..f4f9b17fb8 100644 --- a/erpnext/patches/v13_0/rename_ksa_qr_field.py +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.model.utils.rename_field import rename_field @@ -12,5 +13,20 @@ def execute(): if frappe.db.exists('DocType', 'Sales Invoice'): frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True) + + # rename_field method assumes that the field already exists or the doc is synced + if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'): + create_custom_fields({ + 'Sales Invoice': [ + dict( + fieldname='ksa_einv_qr', + label='KSA E-Invoicing QR', + fieldtype='Attach Image', + read_only=1, no_copy=1, hidden=1 + ) + ] + }) + if frappe.db.has_column('Sales Invoice', 'qr_code'): rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') + frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code") From e74739a67777ec78134fa7ffff6c29d12ccf83de Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 8 Dec 2021 10:39:35 +0530 Subject: [PATCH 12/28] fix: hsn-wise summary is incorrect if an invoice has repeated item code --- .../hsn_wise_summary_of_outward_supplies.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index e03ad374ae..1c1335ebe0 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -114,9 +114,11 @@ def get_items(filters): items = frappe.db.sql(""" select - `tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate, - `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty, - `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount, + `tabSales Invoice Item`.gst_hsn_code, + `tabSales Invoice Item`.stock_uom, + sum(`tabSales Invoice Item`.stock_qty) as stock_qty, + sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount, + sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate, `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, `tabGST HSN Code`.description from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code` @@ -124,6 +126,8 @@ def get_items(filters): and `tabSales Invoice`.docstatus = 1 and `tabSales Invoice Item`.gst_hsn_code is not NULL and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s + group by + `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code """ % (conditions, match_conditions), filters, as_dict=1) From c3e0de28b192263537b4e7df766210904c788c03 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 13 Dec 2021 20:13:53 +0530 Subject: [PATCH 13/28] fix: validate if asset account is set against company --- .../doctype/purchase_invoice/purchase_invoice.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f4fd1bf16e..df957d261b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -297,8 +297,15 @@ class PurchaseInvoice(BuyingController): item.expense_account = stock_not_billed_account elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): - item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, + asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code, company = self.company) + if not asset_category_account: + form_link = get_link_to_form('Asset Category', asset_category) + throw( + _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), + title=_("Missing Account") + ) + item.expense_account = asset_category_account elif item.is_fixed_asset and item.pr_detail: item.expense_account = asset_received_but_not_billed elif not item.expense_account and for_validate: From 6a239c8ea3686de3263d7fd7471ac8ad2c9f99d9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 12 Dec 2021 20:47:48 +0530 Subject: [PATCH 14/28] test: test_hsn_summary_for_invoice_with_duplicate_items --- ...st_hsn_wise_summary_of_outward_supplies.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py new file mode 100644 index 0000000000..86dc458bdb --- /dev/null +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py @@ -0,0 +1,89 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +from unittest import TestCase + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import ( + make_company as setup_company, +) +from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import ( + make_customers as setup_customers, +) +from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import ( + set_account_heads as setup_gst_settings, +) +from erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies import ( + execute as run_report, +) +from erpnext.stock.doctype.item.test_item import make_item + + +class TestHSNWiseSummaryReport(TestCase): + @classmethod + def setUpClass(cls): + setup_company() + setup_customers() + setup_gst_settings() + make_item("Golf Car", properties={ "gst_hsn_code": "999900" }) + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_hsn_summary_for_invoice_with_duplicate_items(self): + si = create_sales_invoice( + company="_Test Company GST", + customer = "_Test GST Customer", + currency = "INR", + warehouse = "Finished Goods - _GST", + debit_to = "Debtors - _GST", + income_account = "Sales - _GST", + expense_account = "Cost of Goods Sold - _GST", + cost_center = "Main - _GST", + do_not_save=1 + ) + + si.items = [] + si.append("items", { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "120", + "cost_center": "Main - _GST" + }) + si.append("items", { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "140", + "cost_center": "Main - _GST" + }) + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18 + }) + si.posting_date = "2020-11-17" + si.submit() + si.reload() + + [columns, data] = run_report(filters=frappe._dict({ + "company": "_Test Company GST", + "gst_hsn_code": "999900", + "company_gstin": si.company_gstin, + "from_date": si.posting_date, + "to_date": si.posting_date + })) + + filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data)) + self.assertTrue(filtered_rows) + + hsn_row = filtered_rows[0] + self.assertEquals(hsn_row['stock_qty'], 2.0) + self.assertEquals(hsn_row['total_amount'], 306.8) From 2137e56947f9b1d3eca281866227aa996e8e7e93 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 13 Dec 2021 22:34:54 +0530 Subject: [PATCH 15/28] revert: "fix: expected value after useful life validation" --- erpnext/assets/doctype/asset/asset.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 65beac0af8..a18b03a888 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -495,7 +495,6 @@ class Asset(AccountsController): asset_value_after_full_schedule = flt( flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation) - flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount')) if (row.expected_value_after_useful_life and From 534c64abcd1bf74a3c4d9fe322516a1410812ab8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Dec 2021 11:55:09 +0530 Subject: [PATCH 16/28] fix: packing slip incorrect item value (#28862) --- .../packing_slip_item/packing_slip_item.json | 543 ++++-------------- 1 file changed, 116 insertions(+), 427 deletions(-) diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json index 29c4193f9e..4270839bfd 100644 --- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json +++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json @@ -1,451 +1,140 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2013-04-08 13:10:16", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2013-04-08 13:10:16", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "batch_no", + "desc_section", + "description", + "quantity_section", + "qty", + "net_weight", + "column_break_10", + "stock_uom", + "weight_uom", + "page_break", + "dn_detail" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "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, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "options": "item_code.item_name", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "200px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "print_width": "200px", + "read_only": 1, "width": "200px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "batch_no", - "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": "Batch No", - "length": 0, - "no_copy": 0, - "options": "Batch", - "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": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "desc_section", - "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": "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 - }, + "collapsible": 1, + "fieldname": "desc_section", + "fieldtype": "Section Break", + "label": "Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 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 Editor", + "label": "Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quantity_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Quantity", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "quantity_section", + "fieldtype": "Section Break", + "label": "Quantity" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Quantity", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "100px", + "reqd": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "net_weight", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Net Weight", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "net_weight", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Net Weight", + "print_width": "100px", "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_10", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "print_width": "100px", + "read_only": 1, "width": "100px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "weight_uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Weight UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "100px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "weight_uom", + "fieldtype": "Link", + "label": "Weight UOM", + "options": "UOM", + "print_width": "100px", "width": "100px" - }, + }, { - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "page_break", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Page Break", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "allow_on_submit": 1, + "default": "0", + "fieldname": "page_break", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Page Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dn_detail", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "DN Detail", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "dn_detail", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "DN Detail" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-06-01 07:21:58.220980", - "modified_by": "Administrator", - "module": "Stock", - "name": "Packing Slip Item", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 1, - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2021-12-14 01:22:00.715935", + "modified_by": "Administrator", + "module": "Stock", + "name": "Packing Slip Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From db46257a3bb6539fc1a45a77eb88912bf8705209 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 14 Dec 2021 12:12:33 +0530 Subject: [PATCH 17/28] feat: added required_date field to set date in child table (#28432) * feat: added required_date field to set date in child table * fix: dont refresh full table Co-authored-by: Ankush Menat --- .../request_for_quotation.js | 16 +++++++++++++++- .../request_for_quotation.json | 9 ++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index bde00cbd94..e7049fd522 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{ dialog.show() }, + schedule_date(frm) { + if(frm.doc.schedule_date){ + frm.doc.items.forEach((item) => { + item.schedule_date = frm.doc.schedule_date; + }) + } + refresh_field("items"); + }, preview: (frm) => { let dialog = new frappe.ui.Dialog({ title: __('Preview Email'), @@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{ dialog.show(); } }) - +frappe.ui.form.on("Request for Quotation Item", { + items_add(frm, cdt, cdn) { + if (frm.doc.schedule_date) { + frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date); + } + } +}); frappe.ui.form.on("Request for Quotation Supplier",{ supplier: function(frm, cdt, cdn) { var d = locals[cdt][cdn] diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 4ce4100a7f..4993df90d1 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -12,6 +12,7 @@ "vendor", "column_break1", "transaction_date", + "schedule_date", "status", "amended_from", "suppliers_section", @@ -246,16 +247,22 @@ "fieldname": "sec_break_email_2", "fieldtype": "Section Break", "hide_border": 1 + }, + { + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required Date" } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-05 22:04:29.017134", + "modified": "2021-11-24 17:47:49.909000", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { From cc8c0595e29d0ab21753d95fbe7db703a50296d4 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 14 Dec 2021 12:20:04 +0530 Subject: [PATCH 18/28] fix(UX): hide RM table(Job Card) if material transfer is against work order (#28746) --- erpnext/manufacturing/doctype/job_card/job_card.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 453ad50e8e..dac7b36f94 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -75,6 +75,15 @@ frappe.ui.form.on('Job Card', { && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } + + if (frm.doc.work_order) { + frappe.db.get_value('Work Order', frm.doc.work_order, + 'transfer_material_against').then((r) => { + if (r.message.transfer_material_against == 'Work Order') { + frm.set_df_property('items', 'hidden', 1); + } + }); + } }, setup_corrective_job_card: function(frm) { From 7fd06108bf79f71877bbc49648120340833eb8bc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Dec 2021 13:03:53 +0530 Subject: [PATCH 19/28] fix: reorder patch to fix custom field options (#28866) --- erpnext/patches.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0687b42631..65ad79ef23 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -165,6 +165,7 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign +erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.rename_pos_closing_doctype @@ -287,7 +288,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego erpnext.patches.v14_0.delete_einvoicing_doctypes erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.set_operation_time_based_on_operating_cost -erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v13_0.fix_invoice_statuses From b027050f77f14d43e4acf82da1cb99b8b00bb8dd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 14 Dec 2021 14:57:21 +0530 Subject: [PATCH 20/28] Merge pull request #28845 from ruchamahabal/fix-salary-slip-timesheet fix: incorrect amount based on payment days in timesheet salary slip --- .../doctype/salary_slip/salary_slip.py | 6 ++- .../doctype/salary_slip/test_salary_slip.py | 51 +++++++++++++++++++ .../doctype/timesheet/test_timesheet.py | 7 ++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 05af09e49d..b035292c0b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -940,10 +940,12 @@ class SalarySlip(TransactionBase): def get_amount_based_on_payment_days(self, row, joining_date, relieving_date): amount, additional_amount = row.amount, row.additional_amount + timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component") + if (self.salary_structure and cint(row.depends_on_payment_days) and cint(self.total_working_days) and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary - and (not self.salary_slip_based_on_timesheet or + and (row.salary_component != timesheet_component or getdate(self.start_date) < joining_date or (relieving_date and getdate(self.end_date) > relieving_date) )): @@ -952,7 +954,7 @@ class SalarySlip(TransactionBase): amount = flt((flt(row.default_amount) * flt(self.payment_days) / cint(self.total_working_days)), row.precision("amount")) + additional_amount - elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days): + elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days): amount, additional_amount = 0, 0 elif not row.amount: amount = flt(row.default_amount) + flt(row.additional_amount) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index e4618c31f2..3052a2b727 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_payment_days_in_salary_slip_based_on_timesheet(self): + from erpnext.hr.doctype.attendance.attendance import mark_attendance + 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 as make_salary_slip_for_timesheet, + ) + + # Payroll based on attendance + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") + + emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company") + frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"}) + + # mark attendance + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + + # salary structure based on timesheet + make_salary_structure_for_timesheet(emp) + timesheet = make_timesheet(emp, simulate=True, is_billable=1) + salary_slip = make_salary_slip_for_timesheet(timesheet.name) + salary_slip.start_date = month_start_date + salary_slip.end_date = month_end_date + salary_slip.save() + salary_slip.submit() + + no_of_days = self.get_no_of_days() + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1) + + # gross pay calculation based on attendance (payment days) + gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days)) + + self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2)) + + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_component_amount_dependent_on_another_payment_days_based_component(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index d59cc01013..148d8ba29c 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase): for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]: frappe.db.sql("delete from `tab%s`" % dt) - if not frappe.db.exists("Salary Component", "Timesheet Component"): - frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert() - - def test_timesheet_billing_amount(self): emp = make_employee("test_employee_6@salary.com") @@ -160,6 +156,9 @@ def make_salary_structure_for_timesheet(employee, company=None): salary_structure_name = "Timesheet Salary Structure Test" frequency = "Monthly" + if not frappe.db.exists("Salary Component", "Timesheet Component"): + frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert() + salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True) salary_structure.salary_component = "Timesheet Component" salary_structure.salary_slip_based_on_timesheet = 1 From 304aa44feb104dbf171facb64151c16b17734289 Mon Sep 17 00:00:00 2001 From: aaronmenezes Date: Tue, 14 Dec 2021 15:18:03 +0530 Subject: [PATCH 21/28] fix: Nonstock item showing in Itemwise Recommended Reorder Level report (#28873) --- .../itemwise_recommended_reorder_level.py | 1 + 1 file changed, 1 insertion(+) 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 314f1608fa..3f490653e1 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 @@ -48,6 +48,7 @@ def get_item_info(filters): conditions = [get_item_group_condition(filters.get("item_group"))] if filters.get("brand"): conditions.append("item.brand=%(brand)s") + conditions.append("is_stock_item = 1") return frappe.db.sql("""select name, item_name, description, brand, item_group, safety_stock, lead_time_days from `tabItem` item where {}""" From 80f1a8c645019f0587acd8116117d37e58e680e3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Dec 2021 17:02:42 +0530 Subject: [PATCH 22/28] fix: incorrect logic for "Reserved Qty for Production" (#28880) * fix: reservation for production incorrect The query uses case to decide what fields to compute reservation on, this case is outermost case hence the very first Work order's "Skip transfer" is considered for ALL work orders. Solution: move the case inside Sum. Steps to reproduce: 1. Make work order for more than 1 qty (with | without skip transfer) 2. Create manufacture and transfer entries. 3. Keep checking reserved quantities during this process. * test: use default warehouse for testing reservation --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 2 +- erpnext/stock/doctype/bin/bin.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 1e74b6dda2..4e83b6511b 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -95,7 +95,7 @@ class TestWorkOrder(ERPNextTestCase): def test_reserved_qty_for_partial_completion(self): item = "_Test Item" - warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC") + warehouse = "_Test Warehouse - _TC" bin1_at_start = get_bin(item, warehouse) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index a33134b491..37b54116a4 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -43,9 +43,9 @@ class Bin(Document): frappe.qb .from_(wo) .from_(wo_item) - .select(Case() - .when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty)) - .else_(Sum(wo_item.required_qty - wo_item.consumed_qty)) + .select(Sum(Case() + .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) + .else_(wo_item.required_qty - wo_item.consumed_qty)) ) .where( (wo_item.item_code == self.item_code) From 66960cf0507649c58d04f31273c8d3255898f173 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Dec 2021 17:12:46 +0530 Subject: [PATCH 23/28] fix: remove set only once from valuation method (#28876) --- erpnext/stock/doctype/item/item.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 4f4e69105a..29abd45fcc 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -361,8 +361,7 @@ "fieldname": "valuation_method", "fieldtype": "Select", "label": "Valuation Method", - "options": "\nFIFO\nMoving Average", - "set_only_once": 1 + "options": "\nFIFO\nMoving Average" }, { "depends_on": "is_stock_item", @@ -1035,7 +1034,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-03 08:32:03.869294", + "modified": "2021-12-14 04:13:16.857534", "modified_by": "Administrator", "module": "Stock", "name": "Item", From 96ef88d9355b00470d14fa1fa703235a8689d94d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Dec 2021 17:36:35 +0530 Subject: [PATCH 24/28] test: remove commits from tests (#27939) * test: remove commits from tests in selling module * test: remove commits from manufacturing module * test: remove commits from accounts module tests * test: remove commits from controller tests * test: keep commits in accounting dimension test * Revert "test: remove commits from controller tests" This reverts commit f5cfbee5da844f8b2bffeee907ac602754f16ff4. * Revert "test: remove commits from accounts module tests" This reverts commit df4c27efddf355e2731a18e8871e54d18590f219. --- .../doctype/blanket_order/test_blanket_order.py | 6 ++---- erpnext/manufacturing/doctype/bom/test_bom.py | 4 ++-- .../doctype/bom_update_tool/test_bom_update_tool.py | 7 ++----- erpnext/manufacturing/doctype/job_card/test_job_card.py | 6 +++--- .../doctype/production_plan/test_production_plan.py | 6 ++---- erpnext/manufacturing/doctype/routing/test_routing.py | 6 ++---- .../manufacturing/doctype/work_order/test_work_order.py | 1 - .../manufacturing/doctype/workstation/test_workstation.py | 6 ++---- erpnext/selling/doctype/customer/test_customer.py | 6 ++---- .../party_specific_item/test_party_specific_item.py | 3 ++- erpnext/selling/doctype/quotation/test_quotation.py | 6 +++--- erpnext/selling/doctype/sales_order/test_sales_order.py | 6 ++++-- .../test_pending_so_items_for_purchase_request.py | 5 ++--- erpnext/selling/report/sales_analytics/test_analytics.py | 5 ++--- 14 files changed, 30 insertions(+), 43 deletions(-) diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index d5db3fc1cc..eff2344e85 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -1,17 +1,15 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe from frappe.utils import add_months, today from erpnext import get_company_currency +from erpnext.tests.utils import ERPNextTestCase from .blanket_order import make_order -class TestBlanketOrder(unittest.TestCase): +class TestBlanketOrder(ERPNextTestCase): def setUp(self): frappe.flags.args = frappe._dict() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 4c032307d8..178d92c26c 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt -import unittest from collections import deque from functools import partial @@ -18,10 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.tests.test_subcontracting import set_backflush_based_on +from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('BOM') -class TestBOM(unittest.TestCase): +class TestBOM(ERPNextTestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 526c243ed7..12576cbf32 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -1,19 +1,16 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - - -import unittest - import frappe from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item +from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('BOM') -class TestBOMUpdateTool(unittest.TestCase): +class TestBOMUpdateTool(ERPNextTestCase): def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 9b4fc8b8b7..bb5004ba86 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1,6 +1,5 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe from frappe.utils import random_string @@ -12,9 +11,10 @@ from erpnext.manufacturing.doctype.job_card.job_card import ( from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestCase -class TestJobCard(unittest.TestCase): +class TestJobCard(ERPNextTestCase): def setUp(self): make_bom_for_jc_tests() @@ -329,4 +329,4 @@ def make_bom_for_jc_tests(): bom.rm_cost_as_per = "Valuation Rate" bom.items[0].uom = "_Test UOM 1" bom.items[0].conversion_factor = 5 - bom.insert() \ No newline at end of file + bom.insert() diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index a2980a732e..2febc1e23c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1,8 +1,5 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe from frappe.utils import add_to_date, flt, now_datetime, nowdate @@ -17,9 +14,10 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) +from erpnext.tests.utils import ERPNextTestCase -class TestProductionPlan(unittest.TestCase): +class TestProductionPlan(ERPNextTestCase): def setUp(self): for item in ['Test Production Item 1', 'Subassembly Item 1', 'Raw Material Item 1', 'Raw Material Item 2']: diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 68d9dec04b..e90b0a7d6d 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -1,17 +1,15 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt - -import unittest - import frappe from frappe.test_runner import make_test_records from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestCase -class TestRouting(unittest.TestCase): +class TestRouting(ERPNextTestCase): @classmethod def setUpClass(cls): cls.item_code = "Test Routing Item - A" diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 4e83b6511b..165e0acebb 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1,6 +1,5 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest import frappe from frappe.utils import add_months, cint, flt, now, today diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 5ed5153528..c298c0a8db 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -1,8 +1,5 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt - -import unittest - import frappe from frappe.test_runner import make_test_records @@ -13,12 +10,13 @@ from erpnext.manufacturing.doctype.workstation.workstation import ( WorkstationHolidayError, check_if_within_operating_hours, ) +from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Warehouse"] test_records = frappe.get_test_records('Workstation') make_test_records('Workstation') -class TestWorkstation(unittest.TestCase): +class TestWorkstation(ERPNextTestCase): 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") diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7d6b74d066..5301fd0524 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -2,8 +2,6 @@ # License: GNU General Public License v3. See license.txt -import unittest - import frappe from frappe.test_runner import make_test_records from frappe.utils import flt @@ -11,7 +9,7 @@ from frappe.utils import flt from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding -from erpnext.tests.utils import create_test_contact_and_address +from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address test_ignore = ["Price List"] test_dependencies = ['Payment Term', 'Payment Terms Template'] @@ -19,7 +17,7 @@ test_records = frappe.get_test_records('Customer') -class TestCustomer(unittest.TestCase): +class TestCustomer(ERPNextTestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index 874a364592..b951044f33 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -6,6 +6,7 @@ import unittest import frappe from erpnext.controllers.queries import item_query +from erpnext.tests.utils import ERPNextTestCase test_dependencies = ['Item', 'Customer', 'Supplier'] @@ -17,7 +18,7 @@ def create_party_specific_item(**args): psi.based_on_value = args.get('based_on_value') psi.insert() -class TestPartySpecificItem(unittest.TestCase): +class TestPartySpecificItem(ERPNextTestCase): def setUp(self): self.customer = frappe.get_last_doc("Customer") self.supplier = frappe.get_last_doc("Supplier") diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index aa83726304..4357201d23 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -1,15 +1,15 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest - import frappe from frappe.utils import add_days, add_months, flt, getdate, nowdate +from erpnext.tests.utils import ERPNextTestCase + test_dependencies = ["Product Bundle"] -class TestQuotation(unittest.TestCase): +class TestQuotation(ERPNextTestCase): def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) self.assertFalse(quotation.get('payment_schedule')) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 2a0752e56a..42bc0b70f8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt import json -import unittest import frappe import frappe.permissions @@ -28,12 +27,14 @@ from erpnext.selling.doctype.sales_order.sales_order import ( ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestCase -class TestSalesOrder(unittest.TestCase): +class TestSalesOrder(ERPNextTestCase): @classmethod def setUpClass(cls): + super().setUpClass() cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order")) @@ -42,6 +43,7 @@ class TestSalesOrder(unittest.TestCase): # reset config to previous state frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) + super().tearDownClass() def tearDown(self): frappe.set_user("Administrator") diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py index 9c30afc5b1..d62915fc66 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py @@ -2,8 +2,6 @@ # For license information, please see license.txt -import unittest - from frappe.utils import add_months, nowdate from erpnext.selling.doctype.sales_order.sales_order import make_material_request @@ -11,9 +9,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import ( execute, ) +from erpnext.tests.utils import ERPNextTestCase -class TestPendingSOItemsForPurchaseRequest(unittest.TestCase): +class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase): def test_result_for_partial_material_request(self): so = make_sales_order() mr=make_material_request(so.name) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 8ffc5d6d0a..f56cce2dfd 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -2,15 +2,14 @@ # For license information, please see license.txt -import unittest - import frappe from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.sales_analytics.sales_analytics import execute +from erpnext.tests.utils import ERPNextTestCase -class TestAnalytics(unittest.TestCase): +class TestAnalytics(ERPNextTestCase): def test_sales_analytics(self): frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") From c5fcbeff7a391cccfb01a75c179ab3bae9e64239 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 14 Dec 2021 18:45:27 +0530 Subject: [PATCH 25/28] fix: reset_expected_response_and_resolution on hold --- .../service_level_agreement.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 50f31fde2d..83ec5e7c8e 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -420,13 +420,13 @@ def handle_status_change(doc, apply_sla_for_resolution): if is_open_status(prev_status) and is_hold_status(doc.status): # Issue is on hold -> Set on_hold_since doc.on_hold_since = now_time + reset_expected_response_and_resolution(doc) # Replied to Open if is_hold_status(prev_status) and is_open_status(doc.status): # Issue was on hold -> Calculate Total Hold Time calculate_hold_hours() # Issue is open -> reset resolution_date - reset_expected_response_and_resolution(doc) reset_resolution_metrics(doc) # Open to Closed @@ -440,7 +440,6 @@ def handle_status_change(doc, apply_sla_for_resolution): # Issue was closed -> Calculate Total Hold Time from resolution_date calculate_hold_hours() # Issue is open -> reset resolution_date - reset_expected_response_and_resolution(doc) reset_resolution_metrics(doc) # Closed to Replied @@ -449,6 +448,7 @@ def handle_status_change(doc, apply_sla_for_resolution): calculate_hold_hours() # Issue is on hold -> Set on_hold_since doc.on_hold_since = now_time + reset_expected_response_and_resolution(doc) # Replied to Closed if is_hold_status(prev_status) and is_fulfilled_status(doc.status): @@ -645,7 +645,7 @@ def on_communication_update(doc, status): if ( doc.sent_or_received == "Received" # a reply is received and parent.get('status') == 'Open' # issue status is set as open from communication.py - and parent._doc_before_save + and parent.get_doc_before_save() and parent.get('status') != parent._doc_before_save.get('status') # status changed ): # undo the status change in db @@ -655,13 +655,16 @@ def on_communication_update(doc, status): elif ( doc.sent_or_received == "Sent" # a reply is sent and parent.get('first_responded_on') # first_responded_on is set from communication.py - and parent._doc_before_save + and parent.get_doc_before_save() and not parent._doc_before_save.get('first_responded_on') # first_responded_on was not set ): # reset first_responded_on since it will be handled/set later on parent.first_responded_on = None parent.flags.on_first_reply = True + else: + return + handle_status_change(parent, for_resolution) update_response_and_resolution_metrics(parent, for_resolution) update_agreement_status(parent, for_resolution) From 7c33df5d544919b40f7e190eb1a4ed53e97e1fd8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 14 Dec 2021 19:32:31 +0530 Subject: [PATCH 26/28] chore: bump coverage requirements --- codecov.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/codecov.yml b/codecov.yml index 67bd4454ef..1fa602ab30 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,6 +8,16 @@ coverage: target: auto threshold: 0.5% + patch: + default: + target: 85% + threshold: 0% + base: auto + branches: + - develop + if_ci_failed: ignore + only_pulls: true + comment: layout: "diff, files" require_changes: true From 0add80b02f9b3c1e342ecf3f50bcb1343efbfa0f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 14 Dec 2021 20:24:29 +0530 Subject: [PATCH 27/28] fix: failing tests --- .../service_level_agreement.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 83ec5e7c8e..c94700bdc5 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -640,8 +640,6 @@ def on_communication_update(doc, status): if not parent.meta.has_field('service_level_agreement'): return - for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') - if ( doc.sent_or_received == "Received" # a reply is received and parent.get('status') == 'Open' # issue status is set as open from communication.py @@ -650,7 +648,11 @@ def on_communication_update(doc, status): ): # undo the status change in db # since prev status is fetched from db - frappe.db.set_value(parent.doctype, parent.name, 'status', parent._doc_before_save.get('status')) + frappe.db.set_value( + parent.doctype, parent.name, + 'status', parent._doc_before_save.get('status'), + update_modified=False + ) elif ( doc.sent_or_received == "Sent" # a reply is sent @@ -665,6 +667,8 @@ def on_communication_update(doc, status): else: return + for_resolution = frappe.db.get_value('Service Level Agreement', parent.service_level_agreement, 'apply_sla_for_resolution') + handle_status_change(parent, for_resolution) update_response_and_resolution_metrics(parent, for_resolution) update_agreement_status(parent, for_resolution) @@ -673,12 +677,10 @@ def on_communication_update(doc, status): def reset_expected_response_and_resolution(doc): - update_values = {} if doc.meta.has_field("first_responded_on") and not doc.get('first_responded_on'): - update_values['response_by'] = None + doc.response_by = None if doc.meta.has_field("resolution_by") and not doc.get('resolution_date'): - update_values['resolution_by'] = None - doc.db_set(update_values) + doc.resolution_by = None def set_response_by(doc, start_date_time, priority): From a481a7c90e1791e4eb3300487c89a78223f668a5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 14 Dec 2021 22:03:44 +0530 Subject: [PATCH 28/28] fix(test): Employee Exit Summary (#28888) --- erpnext/hr/doctype/exit_interview/test_exit_interview.py | 4 ++-- erpnext/hr/report/employee_exits/employee_exits.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/exit_interview/test_exit_interview.py b/erpnext/hr/doctype/exit_interview/test_exit_interview.py index a0bf9b32ec..8e076edf0b 100644 --- a/erpnext/hr/doctype/exit_interview/test_exit_interview.py +++ b/erpnext/hr/doctype/exit_interview/test_exit_interview.py @@ -19,7 +19,7 @@ class TestExitInterview(unittest.TestCase): frappe.db.sql('delete from `tabExit Interview`') def test_duplicate_interview(self): - employee = make_employee('employeeexit1@example.com') + employee = make_employee('employeeexitint1@example.com') frappe.db.set_value('Employee', employee, 'relieving_date', getdate()) interview = create_exit_interview(employee) @@ -27,7 +27,7 @@ class TestExitInterview(unittest.TestCase): self.assertRaises(frappe.DuplicateEntryError, doc.save) def test_relieving_date_validation(self): - employee = make_employee('employeeexit2@example.com') + employee = make_employee('employeeexitint2@example.com') # unset relieving date frappe.db.set_value('Employee', employee, 'relieving_date', None) diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py index 8e0b07d3e1..bde5a89926 100644 --- a/erpnext/hr/report/employee_exits/employee_exits.py +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -108,12 +108,11 @@ def get_data(filters): interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement')) .distinct() - .orderby(employee.relieving_date, order=Order.asc) .where( ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) & ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2))) - ) + ).orderby(employee.relieving_date, order=Order.asc) ) query = get_conditions(filters, query, employee, interview, fnf)