From db3d2c31a3a6809a2a1e9faba331223c63959cc5 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:27:06 +0530 Subject: [PATCH 01/97] fix: Prevent clearing of Depreciation Schedule on adding more than one Finance Book (cherry picked from commit a4043c035d4116e206e486d4eecabe897cf24630) --- 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 beb46f37ab1..dc7160eed0d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -186,11 +186,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 54cac351cdb1b087374866f018051092ade398e8 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Fri, 19 Nov 2021 05:35:41 +0530 Subject: [PATCH 02/97] fix: Rename loop variable (cherry picked from commit 059d1f3b74b2bee707387cea518eeb394bfce3d8) # Conflicts: # erpnext/assets/doctype/asset/asset.py --- erpnext/assets/doctype/asset/asset.py | 72 ++++++++++++++++----------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index dc7160eed0d..5b0601cc4c0 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -188,22 +188,28 @@ 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 +<<<<<<< HEAD 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)) +>>>>>>> 059d1f3b74 (fix: Rename loop variable) 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 +219,66 @@ 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 +<<<<<<< HEAD if has_pro_rata and not self.opening_accumulated_depreciation and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, self.available_for_use_date, d.depreciation_start_date) +======= + if has_pro_rata and n==0: + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + self.available_for_use_date, finance_book.depreciation_start_date) +>>>>>>> 059d1f3b74 (fix: Rename loop variable) # 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, +<<<<<<< HEAD (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) +======= + n * cint(finance_book.frequency_of_depreciation)) +>>>>>>> 059d1f3b74 (fix: Rename loop variable) 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 +289,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 +302,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 +328,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 8a4019e501fc7d0ad93b95b83e701d3d3b3c5c48 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Mon, 22 Nov 2021 21:23:27 +0530 Subject: [PATCH 03/97] fix: Clear Depreciation Schedule entries that aren't linked with Journal Entries before modifying the schedule (cherry picked from commit 475d8394e413d530ec57d9c7f713fecd706b2271) --- 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 5b0601cc4c0..9883442ffe3 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -215,7 +215,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 @@ -341,14 +342,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 57f41bc30a440187368f17137bcf1a98cfbdd319 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Tue, 23 Nov 2021 23:26:32 +0530 Subject: [PATCH 04/97] fix: Test if multiple Depreciation Schedules are set up for multiple Finance Books (cherry picked from commit f455de2924f7c7554b9eefc757c354992901136b) --- 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 ba4dbee72da..30a1d771817 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -976,6 +976,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 ae875ab021edc574220b0df70ef017fe86d562e4 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 02:45:17 +0530 Subject: [PATCH 05/97] fix: Retain depreciation schedule rows that are linked with JEs while clearing the schedule (cherry picked from commit 33a0b1db2ced1fe84386dbd6b1e5eacb535e5098) --- 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 9883442ffe3..36d68b577ee 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -351,6 +351,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: @@ -360,11 +365,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 855ae399b5414a5f912c1256cf5f209559e55575 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 03:19:36 +0530 Subject: [PATCH 06/97] fix: Test if clear_depreciation_schedule() works for multiple finance books (cherry picked from commit 6ec5a190631b563804748fe43532b70d0f230ff9) --- 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 30a1d771817..8103db3acc4 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -976,6 +976,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 d000782de0353f2130f828d31864d95474b39d88 Mon Sep 17 00:00:00 2001 From: GangaManoj Date: Wed, 24 Nov 2021 05:21:31 +0530 Subject: [PATCH 07/97] fix: Edit dates and frequency of depreciation (cherry picked from commit 4311936b7dd2bf7e3f5e11fe5ce6eeff03a7d49b) --- 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 8103db3acc4..1f30bbfaff5 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -986,28 +986,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 c9a9d0bab8ba50d511d68e75e9428dbb607f7ed4 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 29 Dec 2021 21:47:04 -0500 Subject: [PATCH 08/97] fix(ux): tripple digit cart qty --- erpnext/public/scss/shopping_cart.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index e07bcbd28ec..1e01c474c2b 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -588,7 +588,6 @@ body.product-page { top: -10px; left: -12px; background: var(--red-600); - width: 16px; align-items: center; height: 16px; font-size: 10px; From 852543cbcd368ada8a9e10033d99bb1473ec90d4 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Wed, 12 Jan 2022 14:05:09 +0530 Subject: [PATCH 09/97] fix: Merge conflicts --- erpnext/assets/doctype/asset/asset.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 36d68b577ee..264ead6e003 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -192,14 +192,8 @@ class Asset(AccountsController): self.validate_asset_finance_books(finance_book) # value_after_depreciation - current Asset value -<<<<<<< HEAD - 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)) ->>>>>>> 059d1f3b74 (fix: Rename loop variable) + value_after_depreciation = flt(finance_book.value_after_depreciation) else: value_after_depreciation = (flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)) @@ -248,15 +242,9 @@ class Asset(AccountsController): break # For first row -<<<<<<< HEAD if has_pro_rata and not self.opening_accumulated_depreciation and n==0: - depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, - self.available_for_use_date, d.depreciation_start_date) -======= - if has_pro_rata and n==0: depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, self.available_for_use_date, finance_book.depreciation_start_date) ->>>>>>> 059d1f3b74 (fix: Rename loop variable) # 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 @@ -267,11 +255,7 @@ class Asset(AccountsController): 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, -<<<<<<< HEAD - (n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation)) -======= - n * cint(finance_book.frequency_of_depreciation)) ->>>>>>> 059d1f3b74 (fix: Rename loop variable) + (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) depreciation_amount_without_pro_rata = depreciation_amount From d7f16c4924642ff60b223eaf2fdfdd374b266c05 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 18 Jan 2022 11:17:32 +0530 Subject: [PATCH 10/97] refactor: convert CTE validation to python (#29324) Some old servers running on mariadb <10.2 can't work with CTEs hence reverting to basic python code Co-authored-by: Ankush Menat --- erpnext/stock/stock_ledger.py | 41 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f43d82d8b59..b47097553ba 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate +from frappe.utils import cint, cstr, flt, get_datetime, get_link_to_form, getdate, now, nowdate from six import iteritems import erpnext @@ -1139,26 +1139,31 @@ def get_future_sle_with_negative_qty(args): def get_future_sle_with_negative_batch_qty(args): - return frappe.db.sql(""" - with batch_ledger as ( - select - posting_date, posting_time, voucher_type, voucher_no, - sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total - from `tabStock Ledger Entry` - where - item_code = %(item_code)s - and warehouse = %(warehouse)s - and batch_no=%(batch_no)s - and is_cancelled = 0 - order by posting_date, posting_time, creation - ) - select * from batch_ledger + batch_ledger = frappe.db.sql(""" + select + posting_date, posting_time, voucher_type, voucher_no, actual_qty + from `tabStock Ledger Entry` where - cumulative_total < 0.0 - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) - limit 1 + item_code = %(item_code)s + and warehouse = %(warehouse)s + and batch_no=%(batch_no)s + and is_cancelled = 0 + order by timestamp(posting_date, posting_time), creation """, args, as_dict=1) + cumulative_total = 0.0 + current_posting_datetime = get_datetime(str(args.posting_date) + " " + str(args.posting_time)) + for entry in batch_ledger: + cumulative_total += entry.actual_qty + if cumulative_total > -1e-6: + continue + + if (get_datetime(str(entry.posting_date) + " " + str(entry.posting_time)) + >= current_posting_datetime): + + entry.cumulative_total = cumulative_total + return [entry] + def _round_off_if_near_zero(number: float, precision: int = 6) -> float: """ Rounds off the number to zero only if number is close to zero for decimal From 69fded250c9dfb3e82f670a405ef5416c799b185 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 18 Jan 2022 13:33:29 +0530 Subject: [PATCH 11/97] fix: enable allow negative stock by default in reposts (#29331) (#29333) Negative stock validation is done BEFORE reposting as such it's not required to be performed again and most likely this fails on old intermediate data where user has no clue why it failed. (cherry picked from commit fabe0bce15b3bb1941316072304b244161de3a3e) Co-authored-by: Ankush Menat --- .../repost_item_valuation/repost_item_valuation.json | 11 ++++++----- .../repost_item_valuation/repost_item_valuation.py | 3 +-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index cd7e63b18b2..0ba97d59a14 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "REPOST-ITEM-VAL-.######", - "creation": "2020-10-22 22:27:07.742161", + "creation": "2022-01-11 15:03:38.273179", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -129,7 +129,7 @@ "reqd": 1 }, { - "default": "0", + "default": "1", "fieldname": "allow_negative_stock", "fieldtype": "Check", "label": "Allow Negative Stock" @@ -177,7 +177,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-24 02:18:10.524560", + "modified": "2022-01-18 10:57:33.450907", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", @@ -227,5 +227,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" -} + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 5ad8f443203..3b76301b4cc 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -27,8 +27,7 @@ class RepostItemValuation(Document): self.item_code = None self.warehouse = None - self.allow_negative_stock = self.allow_negative_stock or \ - cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + self.allow_negative_stock = 1 def set_company(self): if self.based_on == "Transaction": From c8eae9dcef164182932dd4a94bde451a89fee456 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 18 Jan 2022 16:14:45 +0530 Subject: [PATCH 12/97] fix: contact duplication on converting lead to customer --- erpnext/selling/doctype/customer/customer.py | 70 +++++--------------- 1 file changed, 16 insertions(+), 54 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index c86e18ab7aa..fc9bcc9b934 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -142,7 +142,7 @@ class Customer(TransactionBase): self.update_lead_status() if self.flags.is_new_doc: - self.create_lead_address_contact() + self.link_lead_address_contact() self.update_customer_groups() @@ -176,63 +176,25 @@ class Customer(TransactionBase): if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") - def create_lead_address_contact(self): + def link_lead_address_contact(self): if self.lead_name: # assign lead address to customer (if already not set) - address_names = frappe.get_all('Dynamic Link', filters={ - "parenttype":"Address", - "link_doctype":"Lead", - "link_name":self.lead_name - }, fields=["parent as name"]) + linked_contacts_and_addresses = frappe.get_all('Dynamic Link', filters=[ + ["parenttype", "in", ["Contact", "Address"]], + ["link_doctype", "=", "Lead"], + ["link_name", "=", self.lead_name] + ], fields=[ + "parent as name", + "parenttype as doctype" + ] + ) - for address_name in address_names: - address = frappe.get_doc('Address', address_name.get('name')) - if not address.has_link('Customer', self.name): - address.append('links', dict(link_doctype='Customer', link_name=self.name)) - address.save(ignore_permissions=self.flags.ignore_permissions) + for row in linked_contacts_and_addresses: + linked_doc = frappe.get_doc(row.doctype, row.name) + if not linked_doc.has_link('Customer', self.name): + linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name)) + linked_doc.save(ignore_permissions=self.flags.ignore_permissions) - lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True) - - if not lead.lead_name: - frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name)) - - if lead.organization_lead: - contact_names = frappe.get_all('Dynamic Link', filters={ - "parenttype":"Contact", - "link_doctype":"Lead", - "link_name":self.lead_name - }, fields=["parent as name"]) - - for contact_name in contact_names: - contact = frappe.get_doc('Contact', contact_name.get('name')) - if not contact.has_link('Customer', self.name): - contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - contact.save(ignore_permissions=self.flags.ignore_permissions) - - else: - lead.lead_name = lead.lead_name.lstrip().split(" ") - lead.first_name = lead.lead_name[0] - lead.last_name = " ".join(lead.lead_name[1:]) - - # create contact from lead - contact = frappe.new_doc('Contact') - contact.first_name = lead.first_name - contact.last_name = lead.last_name - contact.gender = lead.gender - contact.salutation = lead.salutation - contact.email_id = lead.email_id - contact.phone = lead.phone - contact.mobile_no = lead.mobile_no - contact.is_primary_contact = 1 - contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - if lead.email_id: - contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) - if lead.mobile_no: - contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) - contact.flags.ignore_permissions = self.flags.ignore_permissions - contact.autoname() - if not frappe.db.exists("Contact", contact.name): - contact.insert() def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): From 79ce36c19f3369754f44baab935081c343ab0ef1 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 18 Jan 2022 16:36:40 +0530 Subject: [PATCH 13/97] fix: sider issue --- erpnext/selling/doctype/customer/customer.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index fc9bcc9b934..df3d9e862e5 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -179,15 +179,15 @@ class Customer(TransactionBase): def link_lead_address_contact(self): if self.lead_name: # assign lead address to customer (if already not set) - linked_contacts_and_addresses = frappe.get_all('Dynamic Link', filters=[ - ["parenttype", "in", ["Contact", "Address"]], - ["link_doctype", "=", "Lead"], - ["link_name", "=", self.lead_name] - ], fields=[ - "parent as name", - "parenttype as doctype" - ] - ) + linked_contacts_and_addresses = frappe.get_all( + "Dynamic Link", + filters=[ + ["parenttype", "in", ["Contact", "Address"]], + ["link_doctype", "=", "Lead"], + ["link_name", "=", self.lead_name], + ], + fields=["parent as name", "parenttype as doctype"], + ) for row in linked_contacts_and_addresses: linked_doc = frappe.get_doc(row.doctype, row.name) From 8775154fa0dbeb9561156ce5d95eba48a6d75b61 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 13 Dec 2021 16:50:55 +0530 Subject: [PATCH 14/97] refactor: update_serial_no function and code cleanup (cherry picked from commit 4f52b86d7e577741f65932601c6ff2066bba1553) # Conflicts: # erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js --- .../maintenance_schedule.py | 26 +++++---- .../maintenance_visit/maintenance_visit.js | 55 +++++++++---------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 2ffae1a4f2a..9c35f4f2457 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - import frappe from frappe import _, throw from frappe.utils import add_days, cint, cstr, date_diff, formatdate, getdate @@ -306,13 +305,18 @@ class MaintenanceSchedule(TransactionBase): return schedule.name @frappe.whitelist() -def update_serial_nos(s_id): - serial_nos = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'serial_no') +def update_serial_nos(item_code, schedule=None): + serial_nos = [] + if schedule: + serial_nos = frappe.db.get_value('Maintenance Schedule Item', { + 'parent': schedule, + 'item_code': item_code + }, 'serial_no') + if serial_nos: serial_nos = get_serial_nos(serial_nos) - return serial_nos - else: - return False + + return serial_nos @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): @@ -320,12 +324,9 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No def update_status_and_detail(source, target, parent): target.maintenance_type = "Scheduled" - target.maintenance_schedule = source.name target.maintenance_schedule_detail = s_id - def update_sales_and_serial(source, target, parent): - sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') - target.service_person = sales_person + def update_serial(source, target, parent): serial_nos = get_serial_nos(target.serial_no) if len(serial_nos) == 1: target.serial_no = serial_nos[0] @@ -346,7 +347,10 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No "Maintenance Schedule Item": { "doctype": "Maintenance Visit Purpose", "condition": lambda doc: doc.item_name == item_name, - "postprocess": update_sales_and_serial + "field_map": { + "sales_person": "service_person" + }, + "postprocess": update_serial } }, target_doc) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 78289679744..b3661b7bf38 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -2,52 +2,50 @@ // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.maintenance"); -var serial_nos = []; frappe.ui.form.on('Maintenance Visit', { - refresh: function (frm) { - //filters for serial_no based on item_code - frm.set_query('serial_no', 'purposes', function (frm, cdt, cdn) { - let item = locals[cdt][cdn]; - if (serial_nos) { - return { - filters: { - 'item_code': item.item_code, - 'name': ["in", serial_nos] - } - }; - } else { - return { - filters: { - 'item_code': item.item_code - } - }; - } - }); - }, setup: function (frm) { frm.set_query('contact_person', erpnext.queries.contact_query); frm.set_query('customer_address', erpnext.queries.address_query); frm.set_query('customer', erpnext.queries.customer); }, - onload: function (frm, cdt, cdn) { - let item = locals[cdt][cdn]; + onload: function (frm) { + // filters for serial no based on item code if (frm.doc.maintenance_type === "Scheduled") { - const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail; + let item_code = frm.doc.purposes[0].item_code; frappe.call({ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos", args: { - s_id: schedule_id - }, - callback: function (r) { - serial_nos = r.message; + schedule: frm.doc.maintenance_schedule, + item_code: item_code } + }).then((r) => { + let serial_nos = r.message; + frm.set_query('serial_no', 'purposes', () => { + if (serial_nos.length > 0) { + return { + filters: { + 'item_code': item_code, + 'name': ["in", serial_nos] + } + }; + } + return { + filters: { + 'item_code': item_code + } + }; + }); }); } if (!frm.doc.status) { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { +<<<<<<< HEAD frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes"); +======= + frm.clear_table("purposes"); +>>>>>>> 4f52b86d7e (refactor: update_serial_no function and code cleanup) frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, @@ -60,7 +58,6 @@ frappe.ui.form.on('Maintenance Visit', { contact_person: function (frm) { erpnext.utils.get_contact_details(frm); } - }) // TODO commonify this code From dc0d974144c4aa97c0c846abdf5afd3f7a8282de Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 15 Dec 2021 15:27:41 +0530 Subject: [PATCH 15/97] fix(patch): updates maintenance schedule field for old docs (cherry picked from commit c872aa43f871dbe8fb71b5584fefab52b42a2eda) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 5 +++++ ...ate_maintenance_schedule_field_in_visit.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8c9b365fa7e..13f0891b89e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -336,8 +336,13 @@ erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_ksa_vat_custom_fields +<<<<<<< HEAD erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning +======= +erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit +erpnext.patches.v14_0.migrate_crm_settings +>>>>>>> c872aa43f8 (fix(patch): updates maintenance schedule field for old docs) diff --git a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py new file mode 100644 index 00000000000..450c00e4212 --- /dev/null +++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py @@ -0,0 +1,22 @@ + +import frappe + + +def execute(): + # Updates the Maintenance Schedule link to fetch serial nos + from frappe.query_builder.functions import Coalesce + mvp = frappe.qb.DocType('Maintenance Visit Purpose') + mv = frappe.qb.DocType('Maintenance Visit') + + frappe.qb.update( + mv + ).join( + mvp + ).on(mvp.parent == mv.name).set( + mv.maintenance_schedule, + Coalesce(mvp.prevdoc_docname, '') + ).where( + (mv.maintenance_type == "Scheduled") + & (mvp.prevdoc_docname.notnull()) + & (mv.docstatus < 2) + ).run(as_dict=1) From f541b5c780b535e324d9e79911233c8eb89acce1 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 17 Dec 2021 11:57:17 +0530 Subject: [PATCH 16/97] test: added test for serial no. filters (cherry picked from commit 9109660a69056424a96bb9ba6d85ccf5214a45c6) --- .../test_maintenance_schedule.py | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 501712613a8..7539505842d 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -8,7 +8,10 @@ from frappe.utils.data import add_days, formatdate, today from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import ( make_maintenance_visit, + update_serial_nos, ) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item # test_records = frappe.get_test_records('Maintenance Schedule') @@ -80,6 +83,41 @@ class TestMaintenanceSchedule(unittest.TestCase): #checks if visit status is back updated in schedule self.assertTrue(ms.schedules[1].completion_status, "Partially Completed") + def test_serial_no_filters(self): + # Without serial no. set in schedule -> returns None + item_code = "_Test Serial Item" + make_serial_item_with_serial(item_code) + ms = make_maintenance_schedule(item_code=item_code) + ms.submit() + + s_item = ms.schedules[0] + mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name) + mvi = mv.purposes[0] + serial_nos = update_serial_nos(mvi.item_name, ms.name) + self.assertEqual(serial_nos, None) + + # With serial no. set in schedule -> returns serial nos. + make_serial_item_with_serial(item_code) + ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002") + ms.submit() + + s_item = ms.schedules[0] + mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name) + mvi = mv.purposes[0] + serial_nos = update_serial_nos(mvi.item_name, ms.name) + self.assertEqual(serial_nos, ["TEST001", "TEST002"]) + + frappe.db.rollback() + +def make_serial_item_with_serial(item_code): + serial_item_doc = create_item(item_code, is_stock_item=1) + if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series: + serial_item_doc.has_serial_no = 1 + serial_item_doc.serial_no_series = "TEST.###" + serial_item_doc.save(ignore_permissions=True) + if frappe.db.exists('Serial No', {"status": "Active", "item_code": item_code}): + make_serialized_item(item_code=item_code) + def get_events(ms): return frappe.get_all("Event Participants", filters={ "reference_doctype": ms.doctype, @@ -87,17 +125,18 @@ def get_events(ms): "parenttype": "Event" }) -def make_maintenance_schedule(): +def make_maintenance_schedule(**args): ms = frappe.new_doc("Maintenance Schedule") ms.company = "_Test Company" ms.customer = "_Test Customer" ms.transaction_date = today() ms.append("items", { - "item_code": "_Test Item", + "item_code": args.get("item_code") or "_Test Item", "start_date": today(), "periodicity": "Weekly", "no_of_visits": 4, + "serial_no": args.get("serial_no"), "sales_person": "Sales Team", }) ms.insert(ignore_permissions=True) From 0bb0a9d0328ea8b9739b13f2261dd17da0671148 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 17 Dec 2021 13:32:30 +0530 Subject: [PATCH 17/97] fix(test): fixed test case not creating serials (cherry picked from commit 90d32006f42a95dba37c51924c5f62be7436dd6f) --- .../doctype/maintenance_schedule/test_maintenance_schedule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 7539505842d..378d3238d75 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -115,7 +115,8 @@ def make_serial_item_with_serial(item_code): serial_item_doc.has_serial_no = 1 serial_item_doc.serial_no_series = "TEST.###" serial_item_doc.save(ignore_permissions=True) - if frappe.db.exists('Serial No', {"status": "Active", "item_code": item_code}): + active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code}) + if len(active_serials) < 2: make_serialized_item(item_code=item_code) def get_events(ms): From 93325c72f0e99af5cb2c4a4dd763ee701c597aa1 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 17 Dec 2021 13:46:46 +0530 Subject: [PATCH 18/97] refactor: moved purpose table mandatory check to server (cherry picked from commit b98c61f2a7247d9f27251b8e740664a77a8b51ef) # Conflicts: # erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js --- .../doctype/maintenance_visit/maintenance_visit.js | 3 +++ .../doctype/maintenance_visit/maintenance_visit.json | 6 +++--- .../doctype/maintenance_visit/maintenance_visit.py | 5 +++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index b3661b7bf38..d08786410c7 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -41,11 +41,14 @@ frappe.ui.form.on('Maintenance Visit', { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { +<<<<<<< HEAD <<<<<<< HEAD frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes"); ======= frm.clear_table("purposes"); >>>>>>> 4f52b86d7e (refactor: update_serial_no function and code cleanup) +======= +>>>>>>> b98c61f2a7 (refactor: moved purpose table mandatory check to server) frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json index ec32239518f..4a6aa0a34bf 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json @@ -179,8 +179,7 @@ "label": "Purposes", "oldfieldname": "maintenance_visit_details", "oldfieldtype": "Table", - "options": "Maintenance Visit Purpose", - "reqd": 1 + "options": "Maintenance Visit Purpose" }, { "fieldname": "more_info", @@ -294,10 +293,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-27 16:06:17.352572", + "modified": "2021-12-17 03:10:27.608112", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py index 5a87b162af6..d5d87536da5 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -18,6 +18,10 @@ class MaintenanceVisit(TransactionBase): if d.serial_no and not frappe.db.exists("Serial No", d.serial_no): frappe.throw(_("Serial No {0} does not exist").format(d.serial_no)) + def validate_purpose_table(self): + if not self.purposes: + frappe.throw(_("Add Items in the Purpose Table"), title="Purposes Required") + def validate_maintenance_date(self): if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail: item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference') @@ -29,6 +33,7 @@ class MaintenanceVisit(TransactionBase): def validate(self): self.validate_serial_no() self.validate_maintenance_date() + self.validate_purpose_table() def update_completion_status(self): if self.maintenance_schedule_detail: From add36363ab4d91f5b821e0ecbcdb032903d6de31 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 17 Jan 2022 18:25:42 +0530 Subject: [PATCH 19/97] fix: Serial No. filters for unscheduled visits (cherry picked from commit dec751377302e504285e834afed6a64dd0f010bf) --- .../maintenance_schedule/maintenance_schedule.py | 2 +- .../maintenance_schedule/test_maintenance_schedule.py | 6 +++--- .../doctype/maintenance_visit/maintenance_visit.js | 11 ++++++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 9c35f4f2457..07d928c221f 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -305,7 +305,7 @@ class MaintenanceSchedule(TransactionBase): return schedule.name @frappe.whitelist() -def update_serial_nos(item_code, schedule=None): +def get_serial_nos_from_schedule(item_code, schedule=None): serial_nos = [] if schedule: serial_nos = frappe.db.get_value('Maintenance Schedule Item', { diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 378d3238d75..4d3c3f48f4a 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -7,8 +7,8 @@ import frappe from frappe.utils.data import add_days, formatdate, today from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import ( + get_serial_nos_from_schedule, make_maintenance_visit, - update_serial_nos, ) from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -93,7 +93,7 @@ class TestMaintenanceSchedule(unittest.TestCase): s_item = ms.schedules[0] mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name) mvi = mv.purposes[0] - serial_nos = update_serial_nos(mvi.item_name, ms.name) + serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name) self.assertEqual(serial_nos, None) # With serial no. set in schedule -> returns serial nos. @@ -104,7 +104,7 @@ class TestMaintenanceSchedule(unittest.TestCase): s_item = ms.schedules[0] mv = make_maintenance_visit(source_name=ms.name, item_name=item_code, s_id=s_item.name) mvi = mv.purposes[0] - serial_nos = update_serial_nos(mvi.item_name, ms.name) + serial_nos = get_serial_nos_from_schedule(mvi.item_name, ms.name) self.assertEqual(serial_nos, ["TEST001", "TEST002"]) frappe.db.rollback() diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index d08786410c7..746fb765e10 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Maintenance Visit', { if (frm.doc.maintenance_type === "Scheduled") { let item_code = frm.doc.purposes[0].item_code; frappe.call({ - method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos", + method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", args: { schedule: frm.doc.maintenance_schedule, item_code: item_code @@ -36,6 +36,15 @@ frappe.ui.form.on('Maintenance Visit', { }; }); }); + } else { + frm.set_query('serial_no', 'purposes', (frm, cdt, cdn) => { + let row = locals[cdt][cdn]; + return { + filters: { + 'item_code': row.item_code + } + }; + }); } if (!frm.doc.status) { frm.set_value({ status: 'Draft' }); From 3023757a602108e8f006066025a7c918d50e29d5 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 19 Jan 2022 13:06:52 +0530 Subject: [PATCH 20/97] fix: Conflicts --- .../doctype/maintenance_visit/maintenance_visit.js | 8 -------- erpnext/patches.txt | 4 ---- 2 files changed, 12 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index 746fb765e10..f4a0d4d399c 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -50,14 +50,6 @@ frappe.ui.form.on('Maintenance Visit', { frm.set_value({ status: 'Draft' }); } if (frm.doc.__islocal) { -<<<<<<< HEAD -<<<<<<< HEAD - frm.doc.maintenance_type == 'Unscheduled' && frm.clear_table("purposes"); -======= - frm.clear_table("purposes"); ->>>>>>> 4f52b86d7e (refactor: update_serial_no function and code cleanup) -======= ->>>>>>> b98c61f2a7 (refactor: moved purpose table mandatory check to server) frm.set_value({ mntc_date: frappe.datetime.get_today() }); } }, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 13f0891b89e..7110a7e308e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -336,13 +336,9 @@ erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.create_ksa_vat_custom_fields -<<<<<<< HEAD erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template erpnext.patches.v13_0.agriculture_deprecation_warning -======= erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit -erpnext.patches.v14_0.migrate_crm_settings ->>>>>>> c872aa43f8 (fix(patch): updates maintenance schedule field for old docs) From 44d209853054930301ae7c03c1d470c4710cecef Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 19 Jan 2022 13:10:23 +0530 Subject: [PATCH 21/97] fix: changed function name --- erpnext/selling/doctype/customer/customer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index df3d9e862e5..df871491422 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -142,7 +142,7 @@ class Customer(TransactionBase): self.update_lead_status() if self.flags.is_new_doc: - self.link_lead_address_contact() + self.link_lead_address_and_contact() self.update_customer_groups() @@ -176,9 +176,9 @@ class Customer(TransactionBase): if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") - def link_lead_address_contact(self): + def link_lead_address_and_contact(self): if self.lead_name: - # assign lead address to customer (if already not set) + # assign lead address and contact to customer (if already not set) linked_contacts_and_addresses = frappe.get_all( "Dynamic Link", filters=[ From 93d665b38a840037476366682873820fc4ca221a Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 19 Jan 2022 13:53:18 +0530 Subject: [PATCH 22/97] fix: Patch (reload doc) --- .../patches/v13_0/update_maintenance_schedule_field_in_visit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py index 450c00e4212..43096991943 100644 --- a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py +++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py @@ -3,6 +3,8 @@ import frappe def execute(): + frappe.reload_doc("maintenance", "doctype", "maintenance_visit") + # Updates the Maintenance Schedule link to fetch serial nos from frappe.query_builder.functions import Coalesce mvp = frappe.qb.DocType('Maintenance Visit Purpose') From f75d36dcb4f96ad9b9b6759fa93ef199b0f95bac Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 19 Jan 2022 19:30:21 +0530 Subject: [PATCH 23/97] fix: from time and to time not updated in drag and drop action #29114 (#29361) fix: from time and to time not updated in drag and drop action (cherry picked from commit 8b5827ed6db1041526b6440ca8e4fde19c646e1e) Co-authored-by: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> --- erpnext/education/api.py | 4 ++-- .../doctype/course_schedule/course_schedule.py | 12 +++++++++++- .../course_schedule/course_schedule_calendar.js | 7 +++---- .../doctype/course_schedule/test_course_schedule.py | 6 ++++++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/erpnext/education/api.py b/erpnext/education/api.py index d9013b08161..636b948a1cc 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None): conditions = get_event_conditions("Course Schedule", filters) data = frappe.db.sql("""select name, course, color, - timestamp(schedule_date, from_time) as from_datetime, - timestamp(schedule_date, to_time) as to_datetime, + timestamp(schedule_date, from_time) as from_time, + timestamp(schedule_date, to_time) as to_time, room, student_group, 0 as 'allDay' from `tabCourse Schedule` where ( schedule_date between %(start)s and %(end)s ) diff --git a/erpnext/education/doctype/course_schedule/course_schedule.py b/erpnext/education/doctype/course_schedule/course_schedule.py index 335b6d28d0c..335cec43527 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule.py +++ b/erpnext/education/doctype/course_schedule/course_schedule.py @@ -3,6 +3,8 @@ # For license information, please see license.txt +from datetime import datetime + import frappe from frappe import _ from frappe.model.document import Document @@ -30,6 +32,14 @@ class CourseSchedule(Document): if self.from_time > self.to_time: frappe.throw(_("From Time cannot be greater than To Time.")) + """Handles specicfic case to update schedule date in calendar """ + if isinstance(self.from_time, str): + try: + datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S') + self.schedule_date = datetime_obj + except ValueError: + pass + def validate_overlap(self): """Validates overlap for Student Group, Instructor, Room""" @@ -47,4 +57,4 @@ class CourseSchedule(Document): validate_overlap_for(self, "Assessment Plan", "student_group") validate_overlap_for(self, "Assessment Plan", "room") - validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) + validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor) \ No newline at end of file diff --git a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js index 803527e5480..cacd539b224 100644 --- a/erpnext/education/doctype/course_schedule/course_schedule_calendar.js +++ b/erpnext/education/doctype/course_schedule/course_schedule_calendar.js @@ -1,11 +1,10 @@ frappe.views.calendar["Course Schedule"] = { field_map: { - // from_datetime and to_datetime don't exist as docfields but are used in onload - "start": "from_datetime", - "end": "to_datetime", + "start": "from_time", + "end": "to_time", "id": "name", "title": "course", - "allDay": "allDay" + "allDay": "allDay", }, gantt: false, order_by: "schedule_date", diff --git a/erpnext/education/doctype/course_schedule/test_course_schedule.py b/erpnext/education/doctype/course_schedule/test_course_schedule.py index a7324195557..56149affcea 100644 --- a/erpnext/education/doctype/course_schedule/test_course_schedule.py +++ b/erpnext/education/doctype/course_schedule/test_course_schedule.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.utils import to_timedelta, today +from frappe.utils.data import add_to_date from erpnext.education.utils import OverlapError @@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase): make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time, student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name) + def test_update_schedule_date(self): + doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1)) + doc.schedule_date = add_to_date(doc.schedule_date, days=1) + doc.save() + def make_course_schedule_test_record(**args): args = frappe._dict(args) From 25398d017bca53f14c7ebb6c9f937ef7c1390093 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 20 Jan 2022 10:51:59 +0530 Subject: [PATCH 24/97] fix: Cleanup empty rows on bank statement import --- .../bank_statement_import.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index c57e862892c..57434bdd829 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -17,6 +17,7 @@ from openpyxl.styles import Font from openpyxl.utils import get_column_letter from six import string_types +INVALID_VALUES = ("", None) class BankStatementImport(DataImport): def __init__(self, *args, **kwargs): @@ -96,6 +97,18 @@ def download_errored_template(data_import_name): data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import.export_errored_rows() +def parse_data_from_template(raw_data): + data = [] + + for i, row in enumerate(raw_data): + if all(v in INVALID_VALUES for v in row): + # empty row + continue + + data.append(row) + + return data + def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options): """This method runs in background job""" @@ -105,7 +118,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url, file = import_file_path if import_file_path else google_sheets_url import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records") - data = import_file.raw_data + + data = parse_data_from_template(import_file.raw_data) if import_file_path: add_bank_account(data, bank_account) From 77cb7ebd4085fc4a1acf3b6b0022ca1ba45b376e Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 20 Jan 2022 13:36:46 +0530 Subject: [PATCH 25/97] fix: (enhance) BOM Operations Report - Added filters in the Report for BOM ID, Item Code and Workstation - Converted Raw SQL to frappe.qb and added method to get filtered data - Changed fieldtype of 'Time in mins' from Int to Float - Get BOM wise grouped data to keep order and accurate grouping in report (cherry picked from commit a9ff1fc52e1be73fa0d4cbb415607d5d545c545c) --- .../bom_operations_time.js | 35 ++++++++- .../bom_operations_time.py | 78 +++++++++++++------ 2 files changed, 89 insertions(+), 24 deletions(-) diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 7468e34020c..705da5a3a69 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -4,6 +4,39 @@ frappe.query_reports["BOM Operations Time"] = { "filters": [ - + { + "fieldname": "item_code", + "label": __("Item Code"), + "fieldtype": "Link", + "width": "100", + "options": "Item", + "get_query": () =>{ + return { + filters: { "disabled": 0, "is_stock_item": 1 } + } + } + }, + { + "fieldname": "bom_id", + "label": __("BOM ID"), + "fieldtype": "MultiSelectList", + "width": "80", + "options": "BOM", + "get_data": function(txt) { + return frappe.db.get_link_options("BOM", txt); + }, + "get_query": () =>{ + return { + filters: { "docstatus": 1, "is_active": 1 } + } + } + }, + { + "fieldname": "workstation", + "label": __("Workstation"), + "fieldtype": "Link", + "width": "100", + "options": "Workstation" + }, ] }; diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py index e7a818abd5d..0bed46ac70b 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py @@ -4,7 +4,8 @@ import frappe from frappe import _ - +from frappe.model.meta import get_field_precision +from frappe.utils import flt def execute(filters=None): data = get_data(filters) @@ -12,19 +13,15 @@ def execute(filters=None): return columns, data def get_data(filters): - data = [] + bom_wise_data = {} + bom_data, report_data = [], [] - bom_data = [] - for d in frappe.db.sql(""" - SELECT - bom.name, bom.item, bom.item_name, bom.uom, - bomps.operation, bomps.workstation, bomps.time_in_mins - FROM `tabBOM` bom, `tabBOM Operation` bomps - WHERE - bom.docstatus = 1 and bom.is_active = 1 and bom.name = bomps.parent - """, as_dict=1): + bom_operation_data = get_filtered_data(filters) + + for d in bom_operation_data: row = get_args() if d.name not in bom_data: + bom_wise_data[d.name] = [] bom_data.append(d.name) row.update(d) else: @@ -34,14 +31,49 @@ def get_data(filters): "time_in_mins": d.time_in_mins }) - data.append(row) + # maintain BOM wise data for grouping such as: + # {"BOM A": [{Row1}, {Row2}], "BOM B": ...} + bom_wise_data[d.name].append(row) used_as_subassembly_items = get_bom_count(bom_data) - for d in data: - d.used_as_subassembly_items = used_as_subassembly_items.get(d.name, 0) + for d in bom_wise_data: + for row in bom_wise_data[d]: + row.used_as_subassembly_items = used_as_subassembly_items.get(row.name, 0) + report_data.append(row) - return data + return report_data + +def get_filtered_data(filters): + bom = frappe.qb.DocType("BOM") + bom_ops = frappe.qb.DocType("BOM Operation") + + bom_ops_query = ( + frappe.qb.from_(bom) + .join(bom_ops).on(bom.name == bom_ops.parent) + .select( + bom.name, bom.item, bom.item_name, bom.uom, + bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins + ).where( + (bom.docstatus == 1) + & (bom.is_active == 1) + ) + ) + + if filters.get("item_code"): + bom_ops_query = bom_ops_query.where(bom.item == filters.get("item_code")) + + if filters.get("bom_id"): + bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id"))) + + if filters.get("workstation"): + bom_ops_query = bom_ops_query.where( + bom_ops.workstation == filters.get("workstation") + ) + + bom_operation_data = bom_ops_query.run(as_dict=True) + + return bom_operation_data def get_bom_count(bom_data): data = frappe.get_all("BOM Item", @@ -68,13 +100,13 @@ def get_columns(filters): "options": "BOM", "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 220 }, { - "label": _("BOM Item Code"), + "label": _("Item Code"), "options": "Item", "fieldname": "item", "fieldtype": "Link", - "width": 140 + "width": 150 }, { "label": _("Item Name"), "fieldname": "item_name", @@ -85,13 +117,13 @@ def get_columns(filters): "options": "UOM", "fieldname": "uom", "fieldtype": "Link", - "width": 140 + "width": 100 }, { "label": _("Operation"), "options": "Operation", "fieldname": "operation", "fieldtype": "Link", - "width": 120 + "width": 140 }, { "label": _("Workstation"), "options": "Workstation", @@ -101,11 +133,11 @@ def get_columns(filters): }, { "label": _("Time (In Mins)"), "fieldname": "time_in_mins", - "fieldtype": "Int", - "width": 140 + "fieldtype": "Float", + "width": 120 }, { "label": _("Sub-assembly BOM Count"), "fieldname": "used_as_subassembly_items", "fieldtype": "Int", - "width": 180 + "width": 200 }] From e963d0e8e0e62b49ecb4afc228afe6ffee958c59 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 20 Jan 2022 14:19:08 +0530 Subject: [PATCH 26/97] fix: Sider (unused imports) and Linter(add extra empty line) (cherry picked from commit 3c7b7721123bb2284a00f84f335a8ab1708831f5) --- .../report/bom_operations_time/bom_operations_time.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py index 0bed46ac70b..eda9eb9d701 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py @@ -4,8 +4,7 @@ import frappe from frappe import _ -from frappe.model.meta import get_field_precision -from frappe.utils import flt + def execute(filters=None): data = get_data(filters) From 655d8980dc60a75f59019f475581c8043de63053 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 20 Jan 2022 14:29:18 +0530 Subject: [PATCH 27/97] fix: Allow only BOM with ops in BOM ID filter & add total row to report (cherry picked from commit af734298c8ac541188de4593a064f962147ea27d) --- .../report/bom_operations_time/bom_operations_time.js | 4 ++-- .../report/bom_operations_time/bom_operations_time.json | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js index 705da5a3a69..0eb22a22f73 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.js @@ -20,14 +20,14 @@ frappe.query_reports["BOM Operations Time"] = { "fieldname": "bom_id", "label": __("BOM ID"), "fieldtype": "MultiSelectList", - "width": "80", + "width": "100", "options": "BOM", "get_data": function(txt) { return frappe.db.get_link_options("BOM", txt); }, "get_query": () =>{ return { - filters: { "docstatus": 1, "is_active": 1 } + filters: { "docstatus": 1, "is_active": 1, "with_operations": 1 } } } }, diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json index 665c5b9f79e..8162017ca81 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.json @@ -1,14 +1,16 @@ { - "add_total_row": 0, + "add_total_row": 1, + "columns": [], "creation": "2020-03-03 01:41:20.862521", "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 0, "is_standard": "Yes", "letter_head": "", - "modified": "2020-03-03 01:41:20.862521", + "modified": "2022-01-20 14:21:47.771591", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operations Time", From b9a42807f8e3542e908ea53e451836cf9e3098df Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 20 Jan 2022 15:06:11 +0530 Subject: [PATCH 28/97] fix: show stock UOM for material transfers (#29376) (#29377) (cherry picked from commit 7e407bcc42260e78ed7275946f71e3e7a89b282f) Co-authored-by: Ankush Menat --- .../manufacturing/doctype/production_plan/production_plan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index e903e9baf84..106777b82ef 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -949,6 +949,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): warehouses, item.get("quantity"), company, ignore_validation=True) required_qty = item.get("quantity") + # get available material by transferring to production warehouse for d in locations: if required_qty <=0: return @@ -959,12 +960,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): new_dict.update({ "quantity": quantity, "material_request_type": "Material Transfer", + "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM "from_warehouse": d.get("warehouse") }) required_qty -= quantity new_mr_items.append(new_dict) + # raise purchase request for remaining qty if required_qty: stock_uom, purchase_uom = frappe.db.get_value( 'Item', From d190ea4ad3b2ed378222894d169fa49f66ecddc8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 20 Jan 2022 14:56:53 +0530 Subject: [PATCH 29/97] ci: disable coverage on v13 branch --- .github/workflows/server-tests.yml | 33 +----------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 6d7324d623b..62576996a0f 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -89,39 +89,8 @@ jobs: run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - name: Run Tests - run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage + run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator env: TYPE: server CI_BUILD_ID: ${{ github.run_id }} ORCHESTRATOR_URL: http://test-orchestrator.frappe.io - - - name: Upload Coverage Data - run: | - cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} - cd ${GITHUB_WORKSPACE} - pip3 install coverage==5.5 - pip3 install coveralls==3.0.1 - coveralls - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_FLAG_NAME: run-${{ matrix.container }} - COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }} - COVERALLS_PARALLEL: true - - coveralls: - name: Coverage Wrap Up - needs: test - container: python:3-slim - runs-on: ubuntu-18.04 - steps: - - name: Clone - uses: actions/checkout@v2 - - - name: Coveralls Finished - run: | - cd ${GITHUB_WORKSPACE} - pip3 install coverage==5.5 - pip3 install coveralls==3.0.1 - coveralls --finish - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 8a9a13cb96be74242b01112ea34f0a568b76e3d6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 20 Jan 2022 15:03:22 +0530 Subject: [PATCH 30/97] ci: concurrency control on v13 branches --- .github/workflows/patch.yml | 4 ++++ .github/workflows/server-tests.yml | 4 ++++ .github/workflows/ui-tests.yml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 8bb44555206..b4421203a15 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -8,6 +8,10 @@ on: workflow_dispatch: +concurrency: + group: patch-mariadb-v13-${{ github.event.number }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-18.04 diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 62576996a0f..c62622eecec 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -12,6 +12,10 @@ on: - '**.js' - '**.md' +concurrency: + group: server-mariadb-v13-${{ github.event.number }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-18.04 diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 5459e86123d..9f142bd2c2f 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -6,6 +6,10 @@ on: - '**.md' workflow_dispatch: +concurrency: + group: ui-v13-${{ github.event.number }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-18.04 From d32f229483cf4681a0873bfc9a4c5d97abecffca Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 20 Jan 2022 15:16:48 +0530 Subject: [PATCH 31/97] ci: fail on merge conflict label --- .github/workflows/patch.yml | 6 ++++++ .github/workflows/server-tests.yml | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index b4421203a15..8d29057b487 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -5,6 +5,7 @@ on: paths-ignore: - '**.js' - '**.md' + types: [opened, unlabeled, synchronize, reopened] workflow_dispatch: @@ -29,6 +30,11 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: + - name: Check for merge conficts label + if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }} + run: | + echo "Remove merge conflicts and remove conflict label to run CI" + exit 1 - name: Clone uses: actions/checkout@v2 diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index c62622eecec..1c9743c5700 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -5,6 +5,7 @@ on: paths-ignore: - '**.js' - '**.md' + types: [opened, unlabeled, synchronize, reopened] workflow_dispatch: push: branches: [ develop ] @@ -39,6 +40,12 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 steps: + - name: Check for merge conficts label + if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }} + run: | + echo "Remove merge conflicts and remove conflict label to run CI" + exit 1 + - name: Clone uses: actions/checkout@v2 From 7d130af12b95483c9046365eafcfcc21c60c397e Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 20 Jan 2022 19:34:36 +0530 Subject: [PATCH 32/97] fix: Cart Logic of Item variant without Website Item --- erpnext/e_commerce/shopping_cart/cart.py | 21 ++++++++++++-- erpnext/e_commerce/variant_selector/utils.py | 29 +++++++++++++++---- .../generators/item/item_configure.js | 2 +- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 03aa36fa883..3681c5baf74 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -42,7 +42,7 @@ def get_cart_quotation(doc=None): if not doc.customer_address and addresses: update_cart_address("billing", addresses[0].name) - + print("doc>>", doc, type(doc)) return { "doc": decorate_quotation_doc(doc), "shipping_addresses": get_shipping_addresses(party), @@ -276,10 +276,25 @@ def guess_territory(): def decorate_quotation_doc(doc): for d in doc.get("items", []): + item_code = d.item_code + fields = ["web_item_name", "thumbnail", "website_image", "description", "route"] + + # Variant Item + if not frappe.db.exists("Website Item", {"item_code": item_code}): + variant_data = frappe.db.get_values( + "Item", + filters={"item_code": item_code}, + fieldname=["variant_of", "item_name"], + as_dict=True + )[0] + item_code = variant_data.variant_of + d.website_item_name = variant_data.item_name + fields = fields[1:] + d.update(frappe.db.get_value( "Website Item", - {"item_code": d.item_code}, - ["web_item_name", "thumbnail", "website_image", "description", "route"], + {"item_code": item_code}, + fields, as_dict=True) ) diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py index 61df3adca58..bccc57b7320 100644 --- a/erpnext/e_commerce/variant_selector/utils.py +++ b/erpnext/e_commerce/variant_selector/utils.py @@ -1,7 +1,9 @@ import frappe from frappe.utils import cint +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager +from erpnext.utilities.product import get_price def get_item_codes_by_attributes(attribute_filters, template_item_code=None): @@ -143,14 +145,13 @@ def get_next_attribute_and_values(item_code, selected_attributes): filtered_items_count = len(filtered_items) # get product info if exact match - from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website + # from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website if exact_match: - data = get_product_info_for_website(exact_match[0]) - product_info = data.product_info + cart_settings = get_shopping_cart_settings() + product_info = get_item_variant_price_dict(exact_match[0], cart_settings) + if product_info: - product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) - if not data.cart_settings.show_price: - product_info = None + product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock) else: product_info = None @@ -195,3 +196,19 @@ def get_item_attributes(item_code): return attributes +def get_item_variant_price_dict(item_code, cart_settings): + if cart_settings.enabled and cart_settings.show_price: + is_guest = frappe.session.user == "Guest" + # Show Price if logged in. + # If not logged in, check if price is hidden for guest. + if not is_guest or not cart_settings.hide_price_for_guest: + price = get_price( + item_code, + cart_settings.price_list, #TODO + cart_settings.default_customer_group, + cart_settings.company + ) + return {"price": price} + + return None + diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js index b5f92989ef4..231ae0587ed 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -214,7 +214,7 @@ class ItemConfigure { ? `