From d8c3935e64f240b938f90558554ef57d6b9f46f0 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 29 Nov 2023 06:15:22 +0530 Subject: [PATCH 01/45] feat: shift depreciation for assets [v14] [backport of #38327] (#38404) feat: shift depreciation for assets --- erpnext/assets/doctype/asset/asset.js | 11 +- erpnext/assets/doctype/asset/asset.py | 96 +++-- erpnext/assets/doctype/asset/test_asset.py | 1 + .../asset_finance_book.json | 10 +- .../asset_shift_allocation/__init__.py | 0 .../asset_shift_allocation.js | 15 + .../asset_shift_allocation.json | 115 +++++ .../asset_shift_allocation.py | 247 +++++++++++ .../test_asset_shift_allocation.py | 112 +++++ .../doctype/asset_shift_factor/__init__.py | 0 .../asset_shift_factor/asset_shift_factor.js | 8 + .../asset_shift_factor.json | 76 ++++ .../asset_shift_factor/asset_shift_factor.py | 24 ++ .../test_asset_shift_factor.py | 9 + .../depreciation_schedule.json | 407 +++++------------- 15 files changed, 796 insertions(+), 335 deletions(-) create mode 100644 erpnext/assets/doctype/asset_shift_allocation/__init__.py create mode 100644 erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js create mode 100644 erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json create mode 100644 erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py create mode 100644 erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py create mode 100644 erpnext/assets/doctype/asset_shift_factor/__init__.py create mode 100644 erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js create mode 100644 erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json create mode 100644 erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py create mode 100644 erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 951e612bea0..afe532d345d 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -323,12 +323,15 @@ frappe.ui.form.on('Asset', { make_schedules_editable: function(frm) { if (frm.doc.finance_books) { - var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 + var is_manual_hence_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 + ? true : false; + var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0 ? true : false; - frm.toggle_enable("schedules", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable); + frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable); } }, diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9708c77f824..e9c1e14b94c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -43,6 +43,7 @@ class Asset(AccountsController): self.validate_finance_books() if not self.split_from: self.prepare_depreciation_data() + self.update_shift_depr_schedule() self.validate_gross_and_purchase_amount() if self.get("schedules"): self.validate_expected_value_after_useful_life() @@ -104,6 +105,13 @@ class Asset(AccountsController): self.opening_accumulated_depreciation ) + def update_shift_depr_schedule(self): + if not any(fb.get("shift_based") for fb in self.finance_books) or self.docstatus != 0: + return + + self.make_depreciation_schedule() + self.set_accumulated_depreciation() + def should_prepare_depreciation_schedule(self): if not self.get("schedules"): return True @@ -318,7 +326,7 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - def make_depreciation_schedule(self, date_of_disposal, value_after_depreciation=None): + def make_depreciation_schedule(self, date_of_disposal=None, value_after_depreciation=None): if not self.get("schedules"): self.schedules = [] @@ -410,13 +418,7 @@ class Asset(AccountsController): ) if depreciation_amount > 0: - self._add_depreciation_row( - date_of_disposal, - depreciation_amount, - finance_book.depreciation_method, - finance_book.finance_book, - finance_book.idx, - ) + self._add_depreciation_row(date_of_disposal, depreciation_amount, finance_book, n) break @@ -498,25 +500,27 @@ class Asset(AccountsController): skip_row = True if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0: - self._add_depreciation_row( - schedule_date, - depreciation_amount, - finance_book.depreciation_method, - finance_book.finance_book, - finance_book.idx, - ) + self._add_depreciation_row(schedule_date, depreciation_amount, finance_book, n) + + def _add_depreciation_row(self, schedule_date, depreciation_amount, finance_book, schedule_idx): + if finance_book.shift_based: + shift = ( + self.schedules_before_clearing[schedule_idx].shift + if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx + else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name") + ) + else: + shift = None - def _add_depreciation_row( - self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id - ): self.append( "schedules", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, - "depreciation_method": depreciation_method, - "finance_book": finance_book, - "finance_book_id": finance_book_id, + "depreciation_method": finance_book.depreciation_method, + "finance_book": finance_book.finance_book, + "finance_book_id": finance_book.idx, + "shift": shift, }, ) @@ -545,6 +549,8 @@ class Asset(AccountsController): num_of_depreciations_completed = 0 depr_schedule = [] + self.schedules_before_clearing = self.get("schedules") + 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): @@ -752,10 +758,12 @@ class Asset(AccountsController): and not date_of_return ): book = self.get("finance_books")[cint(d.finance_book_id) - 1] - depreciation_amount += flt( - value_after_depreciation - flt(book.expected_value_after_useful_life), - d.precision("depreciation_amount"), - ) + + if not book.shift_based: + depreciation_amount += flt( + value_after_depreciation - flt(book.expected_value_after_useful_life), + d.precision("depreciation_amount"), + ) d.depreciation_amount = depreciation_amount accumulated_depreciation += d.depreciation_amount @@ -1217,6 +1225,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount): "total_number_of_depreciations": d.total_number_of_depreciations, "frequency_of_depreciation": d.frequency_of_depreciation, "daily_prorata_based": d.daily_prorata_based, + "shift_based": d.shift_based, "salvage_value_percentage": d.salvage_value_percentage, "expected_value_after_useful_life": flt(gross_purchase_amount) * flt(d.salvage_value_percentage / 100), @@ -1385,6 +1394,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb def get_straight_line_or_manual_depr_amount( asset, row, schedule_idx, number_of_pending_depreciations ): + if row.shift_based: + return get_shift_depr_amount(asset, row, schedule_idx) + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -1479,6 +1491,40 @@ def get_straight_line_or_manual_depr_amount( ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) +def get_shift_depr_amount(asset, row, schedule_idx): + if asset.get("__islocal") and not asset.flags.shift_allocation: + return ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + + asset_shift_factors_map = get_asset_shift_factors_map() + shift = ( + asset.schedules_before_clearing[schedule_idx].shift + if len(asset.schedules_before_clearing) > schedule_idx + else None + ) + shift_factor = asset_shift_factors_map.get(shift) if shift else 0 + + shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) for schedule in asset.schedules_before_clearing + ) + + return ( + ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) + / flt(shift_factors_sum) + ) * shift_factor + + +def get_asset_shift_factors_map(): + return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) + + def get_wdv_or_dd_depr_amount( depreciable_value, rate_of_depreciation, diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 3ab53e57ae3..3c7c33607b0 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1706,6 +1706,7 @@ def create_asset(**args): "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, "depreciation_start_date": args.depreciation_start_date, "daily_prorata_based": args.daily_prorata_based or 0, + "shift_based": args.shift_based or 0, }, ) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index 7c059fde2df..172b81d98b0 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -9,6 +9,7 @@ "depreciation_method", "total_number_of_depreciations", "daily_prorata_based", + "shift_based", "column_break_5", "frequency_of_depreciation", "depreciation_start_date", @@ -93,12 +94,19 @@ "fieldname": "daily_prorata_based", "fieldtype": "Check", "label": "Depreciate based on daily pro-rata" + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\"", + "fieldname": "shift_based", + "fieldtype": "Check", + "label": "Depreciate based on shifts" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-03 22:21:52.090191", + "modified": "2023-11-29 03:53:03.591098", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_shift_allocation/__init__.py b/erpnext/assets/doctype/asset_shift_allocation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js new file mode 100644 index 00000000000..2bd41c13773 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js @@ -0,0 +1,15 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Asset Shift Allocation', { + onload: function(frm) { + frm.events.make_schedules_editable(frm); + }, + + make_schedules_editable: function(frm) { + frm.toggle_enable("depreciation_schedule", true); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", false); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", false); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", true); + } +}); \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json new file mode 100644 index 00000000000..d0dc6fcbcf0 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json @@ -0,0 +1,115 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2023-11-29 04:01:33.796458", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_esaa", + "asset", + "naming_series", + "column_break_tdae", + "finance_book", + "amended_from", + "depreciation_schedule_section", + "depreciation_schedule" + ], + "fields": [ + { + "fieldname": "section_break_esaa", + "fieldtype": "Section Break" + }, + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Naming Series", + "options": "ACC-ASA-.YYYY.-", + "reqd": 1 + }, + { + "fieldname": "column_break_tdae", + "fieldtype": "Column Break" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "depreciation_schedule_section", + "fieldtype": "Section Break", + "label": "Depreciation Schedule" + }, + { + "fieldname": "depreciation_schedule", + "fieldtype": "Table", + "label": "Depreciation Schedule", + "options": "Depreciation Schedule" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Shift Allocation", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-11-29 04:06:20.586168", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Shift Allocation", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py new file mode 100644 index 00000000000..d9797b0c24c --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py @@ -0,0 +1,247 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import add_months, cint, flt, get_last_day + +from erpnext.assets.doctype.asset.asset import get_asset_shift_factors_map +from erpnext.assets.doctype.asset.depreciation import is_last_day_of_the_month + + +class AssetShiftAllocation(Document): + def after_insert(self): + self.fetch_and_set_depr_schedule() + + def validate(self): + self.asset_doc = frappe.get_doc("Asset", self.asset) + + self.validate_invalid_shift_change() + self.update_depr_schedule() + + def on_submit(self): + self.update_asset_schedule() + + def fetch_and_set_depr_schedule(self): + if len(self.asset_doc.finance_books) != 1: + frappe.throw(_("Only assets with one finance book allowed in v14.")) + + if not any(fb.get("shift_based") for fb in self.asset_doc.finance_books): + frappe.throw(_("Asset {0} is not using shift based depreciation").format(self.asset)) + + for schedule in self.asset_doc.get("schedules"): + self.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + "depreciation_method": self.asset_doc.finance_books[0].depreciation_method, + "finance_book": self.asset_doc.finance_books[0].finance_book, + "finance_book_id": self.asset_doc.finance_books[0].idx, + }, + ) + + self.flags.ignore_validate = True + self.save() + + def validate_invalid_shift_change(self): + if not self.get("depreciation_schedule") or self.docstatus == 1: + return + + for i, sch in enumerate(self.depreciation_schedule): + if sch.journal_entry and self.asset_doc.schedules[i].shift != sch.shift: + frappe.throw( + _( + "Row {0}: Shift cannot be changed since the depreciation has already been processed" + ).format(i) + ) + + def update_depr_schedule(self): + if not self.get("depreciation_schedule") or self.docstatus == 1: + return + + self.allocate_shift_diff_in_depr_schedule() + + temp_asset_doc = frappe.copy_doc(self.asset_doc) + + temp_asset_doc.flags.shift_allocation = True + + temp_asset_doc.schedules = [] + + for schedule in self.depreciation_schedule: + temp_asset_doc.append( + "schedules", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + "depreciation_method": self.asset_doc.finance_books[0].depreciation_method, + "finance_book": self.asset_doc.finance_books[0].finance_book, + "finance_book_id": self.asset_doc.finance_books[0].idx, + }, + ) + + temp_asset_doc.prepare_depreciation_data() + + self.depreciation_schedule = [] + + for schedule in temp_asset_doc.get("schedules"): + self.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + "depreciation_method": self.asset_doc.finance_books[0].depreciation_method, + "finance_book": self.asset_doc.finance_books[0].finance_book, + "finance_book_id": self.asset_doc.finance_books[0].idx, + }, + ) + + def allocate_shift_diff_in_depr_schedule(self): + asset_shift_factors_map = get_asset_shift_factors_map() + reverse_asset_shift_factors_map = { + asset_shift_factors_map[k]: k for k in asset_shift_factors_map + } + + original_shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.asset_doc.schedules + ) + + new_shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.depreciation_schedule + ) + + diff = new_shift_factors_sum - original_shift_factors_sum + + if diff > 0: + for i, schedule in reversed(list(enumerate(self.depreciation_schedule))): + if diff <= 0: + break + + shift_factor = flt(asset_shift_factors_map.get(schedule.shift)) + + if shift_factor <= diff: + self.depreciation_schedule.pop() + diff -= shift_factor + else: + try: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get( + shift_factor - diff + ) + diff = 0 + except Exception: + frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( + shift_factor - diff + ) + elif diff < 0: + shift_factors = list(asset_shift_factors_map.values()) + desc_shift_factors = sorted(shift_factors, reverse=True) + depr_schedule_len_diff = self.asset_doc.total_number_of_depreciations - len( + self.depreciation_schedule + ) + subsets_result = [] + + if depr_schedule_len_diff > 0: + num_rows_to_add = depr_schedule_len_diff + + while not subsets_result and num_rows_to_add > 0: + find_subsets_with_sum(shift_factors, num_rows_to_add, abs(diff), [], subsets_result) + if subsets_result: + break + num_rows_to_add -= 1 + + if subsets_result: + for i in range(num_rows_to_add): + schedule_date = add_months( + self.depreciation_schedule[-1].schedule_date, + cint(self.asset_doc.frequency_of_depreciation), + ) + + if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date): + schedule_date = get_last_day(schedule_date) + + self.append( + "depreciation_schedule", + { + "schedule_date": schedule_date, + "shift": reverse_asset_shift_factors_map.get(subsets_result[0][i]), + "depreciation_method": self.asset_doc.finance_books[0].depreciation_method, + "finance_book": self.asset_doc.finance_books[0].finance_book, + "finance_book_id": self.asset_doc.finance_books[0].idx, + }, + ) + + if depr_schedule_len_diff <= 0 or not subsets_result: + for i, schedule in reversed(list(enumerate(self.depreciation_schedule))): + diff = abs(diff) + + if diff <= 0: + break + + shift_factor = flt(asset_shift_factors_map.get(schedule.shift)) + + if shift_factor <= diff: + for sf in desc_shift_factors: + if sf - shift_factor <= diff: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(sf) + diff -= sf - shift_factor + break + else: + try: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get( + shift_factor + diff + ) + diff = 0 + except Exception: + frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( + shift_factor + diff + ) + + def update_asset_schedule(self): + self.asset_doc.flags.shift_allocation = True + + self.asset_doc.schedules = [] + + for schedule in self.depreciation_schedule: + self.asset_doc.append( + "schedules", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + "depreciation_method": self.asset_doc.finance_books[0].depreciation_method, + "finance_book": self.asset_doc.finance_books[0].finance_book, + "finance_book_id": self.asset_doc.finance_books[0].idx, + }, + ) + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() + + +def find_subsets_with_sum(numbers, k, target_sum, current_subset, result): + if k == 0 and target_sum == 0: + result.append(current_subset.copy()) + return + if k <= 0 or target_sum <= 0 or not numbers: + return + + # Include the current number in the subset + find_subsets_with_sum( + numbers, k - 1, target_sum - numbers[0], current_subset + [numbers[0]], result + ) + + # Exclude the current number from the subset + find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result) diff --git a/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py new file mode 100644 index 00000000000..078f4327ea7 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py @@ -0,0 +1,112 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import cstr + +from erpnext.assets.doctype.asset.test_asset import create_asset + + +class TestAssetShiftAllocation(FrappeTestCase): + @classmethod + def setUpClass(cls): + create_asset_shift_factors() + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_asset_shift_allocation(self): + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2023-01-01", + purchase_date="2023-01-01", + gross_purchase_amount=120000, + depreciation_start_date="2023-01-31", + total_number_of_depreciations=12, + frequency_of_depreciation=1, + shift_based=1, + submit=1, + ) + + expected_schedules = [ + ["2023-01-31", 10000.0, 10000.0, "Single"], + ["2023-02-28", 10000.0, 20000.0, "Single"], + ["2023-03-31", 10000.0, 30000.0, "Single"], + ["2023-04-30", 10000.0, 40000.0, "Single"], + ["2023-05-31", 10000.0, 50000.0, "Single"], + ["2023-06-30", 10000.0, 60000.0, "Single"], + ["2023-07-31", 10000.0, 70000.0, "Single"], + ["2023-08-31", 10000.0, 80000.0, "Single"], + ["2023-09-30", 10000.0, 90000.0, "Single"], + ["2023-10-31", 10000.0, 100000.0, "Single"], + ["2023-11-30", 10000.0, 110000.0, "Single"], + ["2023-12-31", 10000.0, 120000.0, "Single"], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset.get("schedules") + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation = frappe.get_doc( + {"doctype": "Asset Shift Allocation", "asset": asset.name} + ).insert() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset_shift_allocation.get("depreciation_schedule") + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation = frappe.get_doc("Asset Shift Allocation", asset_shift_allocation.name) + asset_shift_allocation.depreciation_schedule[4].shift = "Triple" + asset_shift_allocation.save() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset_shift_allocation.get("depreciation_schedule") + ] + + expected_schedules = [ + ["2023-01-31", 10000.0, 10000.0, "Single"], + ["2023-02-28", 10000.0, 20000.0, "Single"], + ["2023-03-31", 10000.0, 30000.0, "Single"], + ["2023-04-30", 10000.0, 40000.0, "Single"], + ["2023-05-31", 20000.0, 60000.0, "Triple"], + ["2023-06-30", 10000.0, 70000.0, "Single"], + ["2023-07-31", 10000.0, 80000.0, "Single"], + ["2023-08-31", 10000.0, 90000.0, "Single"], + ["2023-09-30", 10000.0, 100000.0, "Single"], + ["2023-10-31", 10000.0, 110000.0, "Single"], + ["2023-11-30", 10000.0, 120000.0, "Single"], + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation.submit() + + asset.reload() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset.get("schedules") + ] + + self.assertEqual(schedules, expected_schedules) + + +def create_asset_shift_factors(): + shifts = [ + {"doctype": "Asset Shift Factor", "shift_name": "Half", "shift_factor": 0.5, "default": 0}, + {"doctype": "Asset Shift Factor", "shift_name": "Single", "shift_factor": 1, "default": 1}, + {"doctype": "Asset Shift Factor", "shift_name": "Double", "shift_factor": 1.5, "default": 0}, + {"doctype": "Asset Shift Factor", "shift_name": "Triple", "shift_factor": 2, "default": 0}, + ] + + for s in shifts: + frappe.get_doc(s).insert() diff --git a/erpnext/assets/doctype/asset_shift_factor/__init__.py b/erpnext/assets/doctype/asset_shift_factor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js new file mode 100644 index 00000000000..e6552d8370d --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Asset Shift Factor', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json new file mode 100644 index 00000000000..75cbc1d2523 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:shift_name", + "creation": "2023-11-29 03:45:13.247372", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "shift_name", + "shift_factor", + "default" + ], + "fields": [ + { + "fieldname": "shift_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Shift Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "shift_factor", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Shift Factor", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-11-29 04:06:31.723038", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Shift Factor", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py new file mode 100644 index 00000000000..4c275ce092c --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py @@ -0,0 +1,24 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class AssetShiftFactor(Document): + def validate(self): + self.validate_default() + + def validate_default(self): + if self.default: + existing_default_shift_factor = frappe.db.get_value( + "Asset Shift Factor", {"default": 1}, "name" + ) + + if existing_default_shift_factor: + frappe.throw( + _("Asset Shift Factor {0} is set as default currently. Please change it first.").format( + frappe.bold(existing_default_shift_factor) + ) + ) diff --git a/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py new file mode 100644 index 00000000000..75073673c0c --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestAssetShiftFactor(FrappeTestCase): + pass diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json index 35a2c9dd7f3..21f5ae9edb6 100644 --- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json +++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json @@ -1,318 +1,115 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "", - "beta": 0, - "creation": "2016-03-02 15:11:01.278862", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "creation": "2016-03-02 15:11:01.278862", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "finance_book", + "schedule_date", + "depreciation_amount", + "column_break_3", + "accumulated_depreciation_amount", + "journal_entry", + "shift", + "make_depreciation_entry", + "finance_book_id", + "depreciation_method" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_book", - "fieldtype": "Link", - "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": "Finance Book", - "length": 0, - "no_copy": 0, - "options": "Finance Book", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "finance_book", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Finance Book", + "options": "Finance Book", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "schedule_date", - "fieldtype": "Date", - "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": "Schedule Date", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "schedule_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Schedule Date", + "no_copy": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "depreciation_amount", - "fieldtype": "Currency", - "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": "Depreciation Amount", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "depreciation_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Depreciation Amount", + "no_copy": 1, + "options": "Company:company:default_currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accumulated_depreciation_amount", - "fieldtype": "Currency", - "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": "Accumulated Depreciation Amount", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "accumulated_depreciation_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Accumulated Depreciation Amount", + "no_copy": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus==1", - "fieldname": "journal_entry", - "fieldtype": "Link", - "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": "Journal Entry", - "length": 0, - "no_copy": 1, - "options": "Journal Entry", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "eval:doc.docstatus==1", + "fieldname": "journal_entry", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Journal Entry", + "no_copy": 1, + "options": "Journal Entry", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())", - "fieldname": "make_depreciation_entry", - "fieldtype": "Button", - "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": "Make Depreciation Entry", - "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 - }, + "allow_on_submit": 1, + "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())", + "fieldname": "make_depreciation_entry", + "fieldtype": "Button", + "label": "Make Depreciation Entry" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_book_id", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Finance Book Id", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "finance_book_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Finance Book Id", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "depreciation_method", - "fieldtype": "Select", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Depreciation Method", - "length": 0, - "no_copy": 1, - "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "depreciation_method", + "fieldtype": "Select", + "hidden": 1, + "label": "Depreciation Method", + "no_copy": 1, + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "shift", + "fieldtype": "Link", + "label": "Shift", + "options": "Asset Shift Factor" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-05-10 15:12:41.679436", - "modified_by": "Administrator", - "module": "Assets", - "name": "Depreciation Schedule", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-11-29 04:43:04.218580", + "modified_by": "Administrator", + "module": "Assets", + "name": "Depreciation Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file From cec25740eae25dec8519af756636a521faf3e6b5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:06:04 +0530 Subject: [PATCH 02/45] fix: no fstring in translation (#38381) fix: no fstring in translation (#38381) (cherry picked from commit 8f00481c5f7742b120a232622fae7b3f7e3d2e86) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../stock_and_account_value_comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index b1da3ec1bd1..416cf48871a 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -166,4 +166,4 @@ def create_reposting_entries(rows, company): if entries: entries = ", ".join(entries) - frappe.msgprint(_(f"Reposting entries created: {entries}")) + frappe.msgprint(_("Reposting entries created: {0}").format(entries)) From fd6a23cf41997e1ada5adb7d28dcc091979aabcf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:07:42 +0530 Subject: [PATCH 03/45] fix: debit credit mismatch in multi-currecy asset purchase receipt (#38342) fix: debit credit mismatch in multi-currecy asset purchase receipt (#38342) * fix: Debit credit mimatch in multicurrecy asset purchase receipt * test: multi currency purchase receipt * chore: update init files * test: roolback (cherry picked from commit add238c892364ce25f3e7483cb5fd295f9666a56) Co-authored-by: Deepesh Garg --- erpnext/assets/doctype/asset/test_asset.py | 103 ++++++------------ .../purchase_receipt/purchase_receipt.py | 2 +- 2 files changed, 37 insertions(+), 68 deletions(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 3c7c33607b0..21afd5df851 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -142,12 +142,7 @@ class TestAsset(AssetSetup): ("Creditors - _TC", 0.0, 100000.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", - pi.name, - ) + gle = get_gl_entries("Purchase Invoice", pi.name) self.assertSequenceEqual(gle, expected_gle) pi.cancel() @@ -249,12 +244,7 @@ class TestAsset(AssetSetup): ), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Journal Entry' and voucher_no = %s - order by account""", - asset.journal_entry_for_scrap, - ) + gle = get_gl_entries("Journal Entry", asset.journal_entry_for_scrap) self.assertSequenceEqual(gle, expected_gle) restore_asset(asset.name) @@ -316,13 +306,7 @@ class TestAsset(AssetSetup): ("Debtors - _TC", 25000.0, 0.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - + gle = get_gl_entries("Sales Invoice", si.name) self.assertSequenceEqual(gle, expected_gle) si.cancel() @@ -392,13 +376,7 @@ class TestAsset(AssetSetup): ("Debtors - _TC", 40000.0, 0.0), ) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - + gle = get_gl_entries("Sales Invoice", si.name) self.assertSequenceEqual(gle, expected_gle) def test_asset_with_maintenance_required_status_after_sale(self): @@ -526,13 +504,7 @@ class TestAsset(AssetSetup): ("CWIP Account - _TC", 5250.0, 0.0), ) - pr_gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Receipt' and voucher_no = %s - order by account""", - pr.name, - ) - + pr_gle = get_gl_entries("Purchase Receipt", pr.name) self.assertSequenceEqual(pr_gle, expected_gle) pi = make_invoice(pr.name) @@ -545,13 +517,7 @@ class TestAsset(AssetSetup): ("Creditors - _TC", 0.0, 5500.0), ) - pi_gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", - pi.name, - ) - + pi_gle = get_gl_entries("Purchase Invoice", pi.name) self.assertSequenceEqual(pi_gle, expected_gle) asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name") @@ -578,13 +544,7 @@ class TestAsset(AssetSetup): expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0)) - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Asset' and voucher_no = %s - order by account""", - asset_doc.name, - ) - + gle = get_gl_entries("Asset", asset_doc.name) self.assertSequenceEqual(gle, expected_gle) def test_asset_cwip_toggling_cases(self): @@ -607,10 +567,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 1 -- PR with cwip disabled, Asset with cwip enabled @@ -624,10 +581,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 2 -- PR with cwip enabled, Asset with cwip disabled @@ -640,10 +594,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertTrue(gle) # case 3 -- PI with cwip disabled, Asset with cwip enabled @@ -656,10 +607,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertFalse(gle) # case 4 -- PI with cwip enabled, Asset with cwip disabled @@ -672,10 +620,7 @@ class TestAsset(AssetSetup): asset_doc.available_for_use_date = nowdate() asset_doc.calculate_depreciation = 0 asset_doc.submit() - gle = frappe.db.sql( - """select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", - asset_doc.name, - ) + gle = get_gl_entries("Asset", asset_doc.name) self.assertTrue(gle) frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip) @@ -1644,6 +1589,30 @@ class TestDepreciationBasics(AssetSetup): self.assertRaises(frappe.ValidationError, jv.insert) + def test_multi_currency_asset_pr_creation(self): + pr = make_purchase_receipt( + item_code="Macbook Pro", + qty=1, + rate=100.0, + location="Test Location", + supplier="_Test Supplier USD", + currency="USD", + ) + + pr.submit() + self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) + + +def get_gl_entries(doctype, docname): + gl_entry = frappe.qb.DocType("GL Entry") + return ( + frappe.qb.from_(gl_entry) + .select(gl_entry.account, gl_entry.debit, gl_entry.credit) + .where((gl_entry.voucher_type == doctype) & (gl_entry.voucher_no == docname)) + .orderby(gl_entry.account) + .run() + ) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d8c678737ea..bb3cbd2fa6a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -572,7 +572,7 @@ class PurchaseReceipt(BuyingController): ) stock_value_diff = ( - flt(d.net_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount / self.conversion_rate) + flt(d.landed_cost_voucher_amount) ) From d337533907a17ada35bfe0da070e0ee6ae13a24d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:09:08 +0530 Subject: [PATCH 04/45] fix(regional): use net figures for sales calc (#38260) fix(regional): use net figures for sales calc (#38260) (cherry picked from commit 663bb8726c25005ab83a6ca4c82814b82a98a1e2) Co-authored-by: Dany Robert --- erpnext/regional/report/uae_vat_201/uae_vat_201.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 59ef58bfde3..6ef21e52ca1 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -141,7 +141,7 @@ def get_total_emiratewise(filters): return frappe.db.sql( """ select - s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount) + s.vat_emirate as emirate, sum(i.base_net_amount) as total, sum(i.tax_amount) from `tabSales Invoice Item` i inner join `tabSales Invoice` s on @@ -356,7 +356,7 @@ def get_zero_rated_total(filters): frappe.db.sql( """ select - sum(i.base_amount) as total + sum(i.base_net_amount) as total from `tabSales Invoice Item` i inner join `tabSales Invoice` s on @@ -383,7 +383,7 @@ def get_exempt_total(filters): frappe.db.sql( """ select - sum(i.base_amount) as total + sum(i.base_net_amount) as total from `tabSales Invoice Item` i inner join `tabSales Invoice` s on From c71b31e3b8d577d99bfd904faa02c9b6022f6c6a Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDEissler@users.noreply.github.com> Date: Wed, 29 Nov 2023 05:52:47 +0100 Subject: [PATCH 05/45] fix: make create button translatable (#38165) * fix: make all create buttons translatable * style: use double quotes --------- Co-authored-by: PatrickDenis-stack <77415730+PatrickDenis-stack@users.noreply.github.com> Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> (cherry picked from commit 8e4b591ea2ad420a7c861a06414d3bee6e6ee042) --- erpnext/public/js/communication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 7ce8b0913c3..f205d889658 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", { frappe.confirm(__(confirm_msg, [__("Issue")]), () => { frm.trigger('make_issue_from_communication'); }) - }, "Create"); + }, __("Create")); } if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { From 8e370876cc14ec7ec5099b1fad003049680ed8b0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 29 Nov 2023 12:21:11 +0530 Subject: [PATCH 06/45] fix: incorrect ordered qty for Subcontracting Order (#38415) (cherry picked from commit fddf341f444efa89f80ec0fcb4e35b9b840061f5) # Conflicts: # erpnext/controllers/tests/test_subcontracting_controller.py --- .../tests/test_subcontracting_controller.py | 8 +++ .../subcontracting_order.py | 29 ++++++++- .../test_subcontracting_order.py | 62 +++++++++++++++++++ .../subcontracting_receipt.py | 6 +- 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 4ea4fd11b4e..454eca905c5 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -938,6 +938,7 @@ def make_subcontracted_items(): "Subcontracted Item SA5": {}, "Subcontracted Item SA6": {}, "Subcontracted Item SA7": {}, + "Subcontracted Item SA8": {}, } for item, properties in sub_contracted_items.items(): @@ -956,7 +957,12 @@ def make_raw_materials(): "batch_number_series": "BAT.####", }, "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, +<<<<<<< HEAD "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, +======= + "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"}, + "Subcontracted SRM Item 8": {}, +>>>>>>> fddf341f44 (fix: incorrect ordered qty for Subcontracting Order (#38415)) } for item, properties in raw_materials.items(): @@ -980,6 +986,7 @@ def make_service_items(): "Subcontracted Service Item 5": {}, "Subcontracted Service Item 6": {}, "Subcontracted Service Item 7": {}, + "Subcontracted Service Item 8": {}, } for item, properties in service_items.items(): @@ -1003,6 +1010,7 @@ def make_bom_for_subcontracted_items(): "Subcontracted Item SA5": ["Subcontracted SRM Item 5"], "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], "Subcontracted Item SA7": ["Subcontracted SRM Item 1"], + "Subcontracted Item SA8": ["Subcontracted SRM Item 8"], } for item_code, raw_materials in boms.items(): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 0b14d4d9f50..8bc38ccb08e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -8,7 +8,7 @@ from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created from erpnext.controllers.subcontracting_controller import SubcontractingController -from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin @@ -114,7 +114,32 @@ class SubcontractingOrder(SubcontractingController): ): item_wh_list.append([item.item_code, item.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) + update_bin_qty( + item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)} + ) + + @staticmethod + def get_ordered_qty(item_code, warehouse): + table = frappe.qb.DocType("Subcontracting Order") + child = frappe.qb.DocType("Subcontracting Order Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select((child.qty - child.received_qty) * child.conversion_factor) + .where( + (table.docstatus == 1) + & (child.item_code == item_code) + & (child.warehouse == warehouse) + & (child.qty > child.received_qty) + & (table.status != "Completed") + ) + ) + + query = query.run() + + return flt(query[0][0]) if query else 0 def update_reserved_qty_for_subcontracting(self): for item in self.supplied_items: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 6a2983faaaf..34719daabc2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -6,6 +6,7 @@ from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order from erpnext.controllers.subcontracting_controller import ( @@ -566,6 +567,67 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(sco.status, "Closed") self.assertEqual(sco.supplied_items[0].returned_qty, 5) + def test_ordered_qty_for_subcontracting_order(self): + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 8", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA8", + "fg_item_qty": 10, + }, + ] + + ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + ordered_qty = flt(ordered_qty) + + sco = get_subcontracting_order(service_items=service_items) + sco.reload() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + new_ordered_qty = flt(new_ordered_qty) + + self.assertEqual(ordered_qty + 10, new_ordered_qty) + + for row in sco.supplied_items: + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code=row.rm_item_code, + qty=row.required_qty, + basic_rate=100, + ) + + scr = make_subcontracting_receipt(sco.name) + scr.submit() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + + self.assertEqual(ordered_qty, new_ordered_qty) + + scr.reload() + scr.cancel() + + new_ordered_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="ordered_qty", + ) + + self.assertEqual(ordered_qty + 10, new_ordered_qty) + def create_subcontracting_order(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index a83068f73fd..7b679d907e8 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -111,12 +111,12 @@ class SubcontractingReceipt(SubcontractingController): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.update_status_updater_args() self.update_prevdoc_status() - self.update_stock_ledger() - self.make_gl_entries_on_cancel() - self.repost_future_sle_and_gle() self.delete_auto_created_batches() self.set_consumed_qty_in_subcontract_order() self.set_subcontracting_order_status() + self.update_stock_ledger() + self.make_gl_entries_on_cancel() + self.repost_future_sle_and_gle() self.update_status() @frappe.whitelist() From 55e49514453e09e4e743e484fd316586663d5c57 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 29 Nov 2023 12:25:16 +0530 Subject: [PATCH 07/45] chore: `conflicts` --- erpnext/controllers/tests/test_subcontracting_controller.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 454eca905c5..68af142afcb 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -957,12 +957,8 @@ def make_raw_materials(): "batch_number_series": "BAT.####", }, "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, -<<<<<<< HEAD "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, -======= - "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"}, "Subcontracted SRM Item 8": {}, ->>>>>>> fddf341f44 (fix: incorrect ordered qty for Subcontracting Order (#38415)) } for item, properties in raw_materials.items(): From 49296cd5cb97836156c29d644acd147c36c30c50 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:27:03 +0530 Subject: [PATCH 08/45] fix: `OperationalError` while selecting Serial No in `Warranty Claim` (backport #38394) (#38413) * refactor: use arrow function (cherry picked from commit 1763824e5f7ccaefb83aea84db47c92f9e4c9417) * refactor: use DocType `Fetch From` instead of `frm.add_fetch` (cherry picked from commit 01044ca8e95ec4e84e63fcfe4928ca111cf3a75b) # Conflicts: # erpnext/support/doctype/warranty_claim/warranty_claim.json * refactor: use `frm.set_query` to add filters (cherry picked from commit 640dfab827f2e83b9c3ae7ff839c6f94b63b71b2) * refactor: don't use `cur_frm` (cherry picked from commit 9fadf5f42678736567160bb2b06619383146d4ca) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../doctype/warranty_claim/warranty_claim.js | 134 +++++++----------- .../warranty_claim/warranty_claim.json | 20 ++- 2 files changed, 71 insertions(+), 83 deletions(-) diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.js b/erpnext/support/doctype/warranty_claim/warranty_claim.js index 358768eb46c..10cb37f5124 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.js +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.js @@ -4,93 +4,67 @@ frappe.provide("erpnext.support"); frappe.ui.form.on("Warranty Claim", { - 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); + setup: (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); - frm.add_fetch('serial_no', 'item_code', 'item_code'); - frm.add_fetch('serial_no', 'item_name', 'item_name'); - frm.add_fetch('serial_no', 'description', 'description'); - frm.add_fetch('serial_no', 'maintenance_status', 'warranty_amc_status'); - frm.add_fetch('serial_no', 'warranty_expiry_date', 'warranty_expiry_date'); - frm.add_fetch('serial_no', 'amc_expiry_date', 'amc_expiry_date'); - frm.add_fetch('serial_no', 'customer', 'customer'); - frm.add_fetch('serial_no', 'customer_name', 'customer_name'); - frm.add_fetch('item_code', 'item_name', 'item_name'); - frm.add_fetch('item_code', 'description', 'description'); + frm.set_query("serial_no", () => { + let filters = { + company: frm.doc.company, + }; + + if (frm.doc.item_code) { + filters["item_code"] = frm.doc.item_code; + } + + return { filters: filters }; + }); + + frm.set_query("item_code", () => { + return { + filters: { + disabled: 0, + }, + }; + }); }, - onload: function(frm) { - if(!frm.doc.status) { - frm.set_value('status', 'Open'); + + onload: (frm) => { + if (!frm.doc.status) { + frm.set_value("status", "Open"); } }, - customer: function(frm) { + + refresh: (frm) => { + frappe.dynamic_link = { + doc: frm.doc, + fieldname: "customer", + doctype: "Customer", + }; + + if ( + !frm.doc.__islocal && + ["Open", "Work In Progress"].includes(frm.doc.status) + ) { + frm.add_custom_button(__("Maintenance Visit"), () => { + frappe.model.open_mapped_doc({ + method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", + frm: frm, + }); + }); + } + }, + + customer: (frm) => { erpnext.utils.get_party_details(frm); }, - customer_address: function(frm) { + + customer_address: (frm) => { erpnext.utils.get_address_display(frm); }, - contact_person: function(frm) { + + contact_person: (frm) => { erpnext.utils.get_contact_details(frm); - } + }, }); - -erpnext.support.WarrantyClaim = class WarrantyClaim extends frappe.ui.form.Controller { - refresh() { - frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} - - if(!cur_frm.doc.__islocal && - (cur_frm.doc.status=='Open' || cur_frm.doc.status == 'Work In Progress')) { - cur_frm.add_custom_button(__('Maintenance Visit'), - this.make_maintenance_visit); - } - } - - make_maintenance_visit() { - frappe.model.open_mapped_doc({ - method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit", - frm: cur_frm - }) - } -}; - -extend_cscript(cur_frm.cscript, new erpnext.support.WarrantyClaim({frm: cur_frm})); - -cur_frm.fields_dict['serial_no'].get_query = function(doc, cdt, cdn) { - var cond = []; - var filter = [ - ['Serial No', 'docstatus', '!=', 2] - ]; - if(doc.item_code) { - cond = ['Serial No', 'item_code', '=', doc.item_code]; - filter.push(cond); - } - if(doc.customer) { - cond = ['Serial No', 'customer', '=', doc.customer]; - filter.push(cond); - } - return{ - filters:filter - } -} - -cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) { - if(doc.serial_no) { - return{ - doctype: "Serial No", - fields: "item_code", - filters:{ - name: doc.serial_no - } - } - } - else{ - return{ - filters:[ - ['Item', 'docstatus', '!=', 2], - ['Item', 'disabled', '=', 0] - ] - } - } -}; diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json index 45485ca2c2f..58ffd4a35c7 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.json +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json @@ -90,7 +90,8 @@ "fieldname": "serial_no", "fieldtype": "Link", "label": "Serial No", - "options": "Serial No" + "options": "Serial No", + "search_index": 1 }, { "fieldname": "customer", @@ -126,6 +127,8 @@ "options": "fa fa-ticket" }, { + "fetch_from": "serial_no.item_code", + "fetch_if_empty": 1, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -138,6 +141,7 @@ }, { "depends_on": "eval:doc.item_code", + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "label": "Item Name", @@ -147,6 +151,7 @@ }, { "depends_on": "eval:doc.item_code", + "fetch_from": "item_code.description", "fieldname": "description", "fieldtype": "Small Text", "label": "Description", @@ -162,17 +167,24 @@ "width": "50%" }, { + "fetch_from": "serial_no.maintenance_status", + "fetch_if_empty": 1, "fieldname": "warranty_amc_status", "fieldtype": "Select", "label": "Warranty / AMC Status", - "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC" + "options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC", + "search_index": 1 }, { + "fetch_from": "serial_no.warranty_expiry_date", + "fetch_if_empty": 1, "fieldname": "warranty_expiry_date", "fieldtype": "Date", "label": "Warranty Expiry Date" }, { + "fetch_from": "serial_no.amc_expiry_date", + "fetch_if_empty": 1, "fieldname": "amc_expiry_date", "fieldtype": "Date", "label": "AMC Expiry Date" @@ -223,6 +235,7 @@ { "bold": 1, "depends_on": "customer", + "fetch_from": "customer.customer_name", "fieldname": "customer_name", "fieldtype": "Data", "in_global_search": 1, @@ -362,7 +375,8 @@ ], "icon": "fa fa-bug", "idx": 1, - "modified": "2021-11-09 17:26:09.703215", + "links": [], + "modified": "2023-11-28 17:30:35.676410", "modified_by": "Administrator", "module": "Support", "name": "Warranty Claim", From 7fa97fa7b58a83266c669c6fd7d170f66f162154 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 21 Nov 2023 15:14:55 +0530 Subject: [PATCH 09/45] fix: unset discount amount based on coupon code (cherry picked from commit 6518582ed3be49dd3bc6a62745dae3e6faed2658) # Conflicts: # erpnext/public/js/utils/sales_common.js --- erpnext/public/js/utils/sales_common.js | 431 ++++++++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 erpnext/public/js/utils/sales_common.js diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js new file mode 100644 index 00000000000..5514963c966 --- /dev/null +++ b/erpnext/public/js/utils/sales_common.js @@ -0,0 +1,431 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.provide("erpnext.selling"); + +erpnext.sales_common = { + setup_selling_controller:function() { + erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController { + setup() { + super.setup(); + this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales"); + this.frm.email_field = "contact_email"; + } + + onload() { + super.onload(); + this.setup_queries(); + this.frm.set_query('shipping_rule', function() { + return { + filters: { + "shipping_rule_type": "Selling" + } + }; + }); + } + + setup_queries() { + var me = this; + + $.each([["customer", "customer"], + ["lead", "lead"]], + function(i, opts) { + if(me.frm.fields_dict[opts[0]]) + me.frm.set_query(opts[0], erpnext.queries[opts[1]]); + }); + + me.frm.set_query('contact_person', erpnext.queries.contact_query); + me.frm.set_query('customer_address', erpnext.queries.address_query); + me.frm.set_query('shipping_address_name', erpnext.queries.address_query); + me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); + + erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); + + if(this.frm.fields_dict.selling_price_list) { + this.frm.set_query("selling_price_list", function() { + return { filters: { selling: 1 } }; + }); + } + + if(this.frm.fields_dict.tc_name) { + this.frm.set_query("tc_name", function() { + return { filters: { selling: 1 } }; + }); + } + + if(!this.frm.fields_dict["items"]) { + return; + } + + if(this.frm.fields_dict["items"].grid.get_field('item_code')) { + this.frm.set_query("item_code", "items", function() { + return { + query: "erpnext.controllers.queries.item_query", + filters: {'is_sales_item': 1, 'customer': me.frm.doc.customer, 'has_variants': 0} + } + }); + } + + if(this.frm.fields_dict["packed_items"] && + this.frm.fields_dict["packed_items"].grid.get_field('batch_no')) { + this.frm.set_query("batch_no", "packed_items", function(doc, cdt, cdn) { + return me.set_query_for_batch(doc, cdt, cdn) + }); + } + + if(this.frm.fields_dict["items"].grid.get_field('item_code')) { + this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) { + return me.set_query_for_item_tax_template(doc, cdt, cdn) + }); + } + + } + + refresh() { + super.refresh(); + + frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'} + + this.frm.toggle_display("customer_name", + (this.frm.doc.customer_name && this.frm.doc.customer_name!==this.frm.doc.customer)); + + this.toggle_editable_price_list_rate(); + } + + customer() { + var me = this; + erpnext.utils.get_party_details(this.frm, null, null, function() { + me.apply_price_list(); + }); + } + + customer_address() { + erpnext.utils.get_address_display(this.frm, "customer_address"); + erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name"); + } + + shipping_address_name() { + erpnext.utils.get_address_display(this.frm, "shipping_address_name", "shipping_address"); + erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name"); + } + + dispatch_address_name() { + erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address"); + } + + sales_partner() { + this.apply_pricing_rule(); + } + + campaign() { + this.apply_pricing_rule(); + } + + selling_price_list() { + this.apply_price_list(); + this.set_dynamic_labels(); + } + + discount_percentage(doc, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); + item.discount_amount = 0.0; + this.apply_discount_on_item(doc, cdt, cdn, 'discount_percentage'); + } + + discount_amount(doc, cdt, cdn) { + + if(doc.name === cdn) { + return; + } + + var item = frappe.get_doc(cdt, cdn); + item.discount_percentage = 0.0; + this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount'); + } + + commission_rate() { + this.calculate_commission(); + } + + total_commission() { + frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]); + + const { amount_eligible_for_commission } = this.frm.doc; + if(!amount_eligible_for_commission) return; + + this.frm.set_value( + "commission_rate", flt( + this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission + ) + ); + } + + allocated_percentage(doc, cdt, cdn) { + var sales_person = frappe.get_doc(cdt, cdn); + if(sales_person.allocated_percentage) { + + sales_person.allocated_percentage = flt(sales_person.allocated_percentage, + precision("allocated_percentage", sales_person)); + + sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission * + sales_person.allocated_percentage / 100.0, + precision("allocated_amount", sales_person)); + refresh_field(["allocated_amount"], sales_person); + + this.calculate_incentive(sales_person); + refresh_field(["allocated_percentage", "allocated_amount", "commission_rate","incentives"], sales_person.name, + sales_person.parentfield); + } + } + + sales_person(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + this.calculate_incentive(row); + refresh_field("incentives",row.name,row.parentfield); + } + + toggle_editable_price_list_rate() { + var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name); + var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate")); + + if(df && editable_price_list_rate) { + const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item"); + if (!this.frm.fields_dict[parent_field]) return; + + this.frm.fields_dict[parent_field].grid.update_docfield_property( + 'price_list_rate', 'read_only', 0 + ); + } + } + + calculate_commission() { + if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return; + + if(this.frm.doc.commission_rate > 100) { + this.frm.set_value("commission_rate", 100); + frappe.throw(`${__(frappe.meta.get_label( + this.frm.doc.doctype, "commission_rate", this.frm.doc.name + ))} ${__("cannot be greater than 100")}`); + } + + this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce( + (sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0 + ) + + this.frm.doc.total_commission = flt( + this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0, + precision("total_commission") + ); + + refresh_field(["amount_eligible_for_commission", "total_commission"]); + } + + calculate_contribution() { + var me = this; + $.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) { + frappe.model.round_floats_in(sales_person); + if (!sales_person.allocated_percentage) return; + + sales_person.allocated_amount = flt( + me.frm.doc.amount_eligible_for_commission + * sales_person.allocated_percentage + / 100.0, + precision("allocated_amount", sales_person) + ); + }); + } + + calculate_incentive(row) { + if(row.allocated_amount) + { + row.incentives = flt( + row.allocated_amount * row.commission_rate / 100.0, + precision("incentives", row)); + } + } + + set_dynamic_labels() { + super.set_dynamic_labels(); + this.set_product_bundle_help(this.frm.doc); + } + + set_product_bundle_help(doc) { + if(!this.frm.fields_dict.packing_list) return; + if ((doc.packed_items || []).length) { + $(this.frm.fields_dict.packing_list.row.wrapper).toggle(true); + + if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { + var help_msg = "
" + + __("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+ + "
"; + frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = help_msg; + } + } else { + $(this.frm.fields_dict.packing_list.row.wrapper).toggle(false); + if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) { + frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = ''; + } + } + refresh_field('product_bundle_help'); + } + + company_address() { + var me = this; + if(this.frm.doc.company_address) { + frappe.call({ + method: "frappe.contacts.doctype.address.address.get_address_display", + args: {"address_dict": this.frm.doc.company_address }, + callback: function(r) { + if(r.message) { + me.frm.set_value("company_address_display", r.message) + } + } + }) + } else { + this.frm.set_value("company_address_display", ""); + } + } + + conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) { + super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate); + } + + qty(doc, cdt, cdn) { + super.qty(doc, cdt, cdn); + } + + pick_serial_and_batch(doc, cdt, cdn) { + let item = locals[cdt][cdn]; + let me = this; + let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; + + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) + .then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.qty > 0 ? "Outward":"Inward"; + + item.title = item.has_serial_no ? + __("Select Serial No") : __("Select Batch No"); + + if (item.has_serial_no && item.has_batch_no) { + item.title = __("Select Serial and Batch"); + } + + frappe.require(path, function() { + new erpnext.SerialBatchPackageSelector( + me.frm, item, (r) => { + if (r) { + frappe.model.set_value(item.doctype, item.name, { + "serial_and_batch_bundle": r.name, + "qty": Math.abs(r.total_qty) + }); + } + } + ); + }); + } + }); + } + + update_auto_repeat_reference(doc) { + if (doc.auto_repeat) { + frappe.call({ + method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference", + args:{ + docname: doc.auto_repeat, + reference:doc.name + }, + callback: function(r){ + if (r.message=="success") { + frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'}); + } else { + frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'}); + } + } + }) + } + } + + project() { + let me = this; + if(in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) { + if(this.frm.doc.project) { + frappe.call({ + method:'erpnext.projects.doctype.project.project.get_cost_center_name' , + args: {project: this.frm.doc.project}, + callback: function(r, rt) { + if(!r.exc) { + $.each(me.frm.doc["items"] || [], function(i, row) { + if(r.message) { + frappe.model.set_value(row.doctype, row.name, "cost_center", r.message); + frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message])); + } + }) + } + } + }) + } + } + } + + coupon_code() { + this.frm.set_value("discount_amount", 0); + this.frm.set_value("additional_discount_percentage", 0); + } + }; + } +} + +erpnext.pre_sales = { + set_as_lost: function(doctype) { + frappe.ui.form.on(doctype, { + set_as_lost_dialog: function(frm) { + var dialog = new frappe.ui.Dialog({ + title: __("Set as Lost"), + fields: [ + { + "fieldtype": "Table MultiSelect", + "label": __("Lost Reasons"), + "fieldname": "lost_reason", + "options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail', + "reqd": 1 + }, + { + "fieldtype": "Table MultiSelect", + "label": __("Competitors"), + "fieldname": "competitors", + "options": "Competitor Detail" + }, + { + "fieldtype": "Small Text", + "label": __("Detailed Reason"), + "fieldname": "detailed_reason" + }, + ], + primary_action: function() { + let values = dialog.get_values(); + + frm.call({ + doc: frm.doc, + method: 'declare_enquiry_lost', + args: { + 'lost_reasons_list': values.lost_reason, + 'competitors': values.competitors ? values.competitors : [], + 'detailed_reason': values.detailed_reason + }, + callback: function(r) { + dialog.hide(); + frm.reload_doc(); + }, + }); + }, + primary_action_label: __('Declare Lost') + }); + + dialog.show(); + } + }); + } +} \ No newline at end of file From ecde1d58b5f3dbc0bbbf2fc96a867b7e25ae5776 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 29 Nov 2023 18:25:52 +0530 Subject: [PATCH 10/45] fix: set cwip account before asset tests (cherry picked from commit ef8e4191cda22e148e9319758ab6624d76a4ae2f) --- .../asset_value_adjustment/test_asset_value_adjustment.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 977a9b3714b..8fdcd0c14df 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -15,6 +15,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu class TestAssetValueAdjustment(unittest.TestCase): def setUp(self): create_asset_data() + frappe.db.set_value( + "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" + ) def test_current_asset_value(self): pr = make_purchase_receipt( From 2dc4b02fc853ec1cf40ab1da1126811e0f9e2862 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:11:53 +0530 Subject: [PATCH 11/45] fix: use `docstatus` instead of `status` (backport #38439) (#38442) fix: use `docstatus` instead of `status` (cherry picked from commit 1423b38d50faa8b2f44cab9626a837037e1d0570) Co-authored-by: s-aga-r --- erpnext/buying/doctype/purchase_order/purchase_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 69f34c33b69..56c06fb50ce 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -705,8 +705,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): @frappe.whitelist() def is_subcontracting_order_created(po_name) -> bool: - count = frappe.db.count( - "Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]} + return ( + True + if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]}) + else False ) - - return True if count else False From 2908d966b620c4462b96d79e46f08e6bed3ff1df Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:15:52 +0530 Subject: [PATCH 12/45] fix: exploded items in Subcontracting Receipt (backport #38441) (#38444) * fix: exploded items in Subcontracting Receipt (cherry picked from commit 62b4a263f86a15dc39cd62b4fa39df87f0ec0dfa) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../subcontracting_receipt_item.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 28bd84e5895..755142e5a97 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -45,6 +45,7 @@ "subcontracting_receipt_item", "section_break_45", "bom", + "include_exploded_items", "serial_no", "col_break5", "batch_no", @@ -465,12 +466,19 @@ "fieldname": "accounting_details_section", "fieldtype": "Section Break", "label": "Accounting Details" + }, + { + "default": "0", + "fieldname": "include_exploded_items", + "fieldtype": "Check", + "label": "Include Exploded Items", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:38:26.459669", + "modified": "2023-11-30 12:05:51.920705", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From e8ea6022f02615dccb0766b52b9a0c36a3b2d067 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 30 Nov 2023 12:22:55 +0530 Subject: [PATCH 13/45] refactor: pass on filter to upfront outstanding query as well (cherry picked from commit cfd3230c75b5e549b98b2362947ee8744c98e920) --- erpnext/accounts/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2836056b0c9..c98b8193538 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1809,6 +1809,8 @@ class QueryPaymentLedger(object): .where(ple.delinked == 0) .where(Criterion.all(filter_on_against_voucher_no)) .where(Criterion.all(self.common_filter)) + .where(Criterion.all(self.dimensions_filter)) + .where(Criterion.all(self.voucher_posting_date)) .groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party) .orderby(ple.posting_date, ple.voucher_no) .having(qb.Field("amount_in_account_currency") > 0) From b7556f9b30a7ec4f3455ed8279c24447b89bd5bc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 30 Nov 2023 15:57:57 +0530 Subject: [PATCH 14/45] refactor: use flt on outstanding on AR/AP summary report (cherry picked from commit e4bdd3a28d7fa29b0460ad6661ff39a219c1fc5c) --- .../accounts_receivable_summary.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 60274cd8b10..a92f960fdf0 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -8,6 +8,7 @@ from frappe.utils import cint, flt from erpnext.accounts.party import get_partywise_advanced_payment_amount from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport +from erpnext.accounts.utils import get_currency_precision def execute(filters=None): @@ -35,6 +36,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_data(self, args): self.data = [] self.receivables = ReceivablePayableReport(self.filters).run(args)[1] + self.currency_precision = get_currency_precision() or 2 self.get_party_total(args) @@ -58,7 +60,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company) for party, party_dict in self.party_total.items(): - if party_dict.outstanding == 0: + if flt(party_dict.outstanding, self.currency_precision) == 0: continue row = frappe._dict() From bec7fb54936867f438bea60f42637089eedf9f8b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 17:32:58 +0530 Subject: [PATCH 15/45] fix(ux): make valuation field read only when it can't be modified (backport #38450) (#38463) * fix(ux): make `basic_rate` field read-only based on purpose (cherry picked from commit abc7d3002467946984be5b41d79024da92047dd3) * fix(ux): make PR `rate` field read-only having PO ref (cherry picked from commit ae294ee4702fa8e0367c07c881d632811beb67ae) # Conflicts: # erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json * fix(ux): make PI `rate` field read-only having PR ref (cherry picked from commit 3d4156cc7d90522f37f4c91e4ccf4145412109a9) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../purchase_invoice_item/purchase_invoice_item.json | 3 ++- .../purchase_receipt_item/purchase_receipt_item.json | 3 ++- erpnext/stock/doctype/stock_entry/stock_entry.js | 9 +++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index b62b4bab140..bc56132c60f 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -286,6 +286,7 @@ "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency", + "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)", "reqd": 1 }, { @@ -893,7 +894,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:33:48.547297", + "modified": "2023-11-30 16:26:05.629780", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 749dbf2111f..6d89e9897af 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -352,6 +352,7 @@ "oldfieldtype": "Currency", "options": "currency", "print_width": "100px", + "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)", "width": "100px" }, { @@ -1054,7 +1055,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:38:15.251994", + "modified": "2023-11-30 16:12:02.364608", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6dd0a58645c..70771e77461 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -241,7 +241,7 @@ frappe.ui.form.on('Stock Entry', { } } - if (frm.doc.docstatus===0) { + if (frm.doc.docstatus === 0) { frm.add_custom_button(__('Purchase Invoice'), function() { erpnext.utils.map_current_doc({ method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_stock_entry", @@ -294,7 +294,8 @@ frappe.ui.form.on('Stock Entry', { }) }, __("Get Items From")); } - if (frm.doc.docstatus===0 && frm.doc.purpose == "Material Issue") { + + if (frm.doc.docstatus === 0 && frm.doc.purpose == "Material Issue") { frm.add_custom_button(__('Expired Batches'), function() { frappe.call({ method: "erpnext.stock.doctype.stock_entry.stock_entry.get_expired_batch_items", @@ -379,6 +380,10 @@ frappe.ui.form.on('Stock Entry', { frm.remove_custom_button('Bill of Materials', "Get Items From"); frm.events.show_bom_custom_button(frm); frm.trigger('add_to_transit'); + + frm.fields_dict.items.grid.update_docfield_property( + 'basic_rate', 'read_only', frm.doc.purpose == "Material Receipt" ? 0 : 1 + ); }, purpose: function(frm) { From 497049b3fd7139c59b0f117ddaea9fbd2df98e18 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 20:53:52 +0530 Subject: [PATCH 16/45] fix: incorrect requested quantity for the subcontracting order (backport #38455) (#38471) * fix: incorrect requested quantity for the subcontracting order (cherry picked from commit 691e3bb24fe62624cd2c39f88e7663ff690b0f1c) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.py # erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py # erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json # erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json * chore: fix conflicts * chore: fix conflicts * chore: fix conflicts * chore: fix conflicts --------- Co-authored-by: Rohit Waghchaure --- .../doctype/purchase_order/purchase_order.py | 36 +-- .../controllers/subcontracting_controller.py | 17 ++ .../subcontracting_order.py | 25 +- .../test_subcontracting_order.py | 56 ++++ .../subcontracting_order_item.json | 40 ++- .../subcontracting_order_service_item.json | 289 ++++++++++-------- 6 files changed, 309 insertions(+), 154 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 56c06fb50ce..51b3a4771a0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -86,6 +86,10 @@ class PurchaseOrder(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): + mri_compare_fields = [["project", "="], ["item_code", "="]] + if self.is_subcontracted: + mri_compare_fields = [["project", "="]] + super(PurchaseOrder, self).validate_with_previous_doc( { "Supplier Quotation": { @@ -108,7 +112,7 @@ class PurchaseOrder(BuyingController): }, "Material Request Item": { "ref_dn_field": "material_request_item", - "compare_fields": [["project", "="], ["item_code", "="]], + "compare_fields": mri_compare_fields, "is_child_table": True, }, } @@ -282,23 +286,6 @@ class PurchaseOrder(BuyingController): check_list.append(d.material_request) check_on_hold_or_closed_status("Material Request", d.material_request) - def update_requested_qty(self): - material_request_map = {} - for d in self.get("items"): - if d.material_request_item: - material_request_map.setdefault(d.material_request, []).append(d.material_request_item) - - for mr, mr_item_rows in material_request_map.items(): - if mr and mr_item_rows: - mr_obj = frappe.get_doc("Material Request", mr) - - if mr_obj.status in ["Stopped", "Cancelled"]: - frappe.throw( - _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError - ) - - mr_obj.update_requested_qty(mr_item_rows) - def update_ordered_qty(self, po_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] @@ -340,7 +327,9 @@ class PurchaseOrder(BuyingController): self.update_status_updater() self.update_prevdoc_status() - self.update_requested_qty() + if not self.is_subcontracted or self.is_old_subcontracting_flow: + self.update_requested_qty() + self.update_ordered_qty() self.validate_budget() self.update_reserved_qty_for_subcontract() @@ -372,7 +361,9 @@ class PurchaseOrder(BuyingController): # Must be called after updating ordered qty in Material Request # bin uses Material Request Items to recalculate & update - self.update_requested_qty() + if not self.is_subcontracted or self.is_old_subcontracting_flow: + self.update_requested_qty() + self.update_ordered_qty() self.update_blanket_order() @@ -679,7 +670,10 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): }, "Purchase Order Item": { "doctype": "Subcontracting Order Service Item", - "field_map": {}, + "field_map": { + "material_request": "material_request", + "material_request_item": "material_request_item", + }, "field_no_map": [], }, }, diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 6faddd2a8be..34d3e700ccc 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -789,6 +789,23 @@ class SubcontractingController(StockController): return self._sub_contracted_items + def update_requested_qty(self): + material_request_map = {} + for d in self.get("items"): + if d.material_request_item: + material_request_map.setdefault(d.material_request, []).append(d.material_request_item) + + for mr, mr_item_rows in material_request_map.items(): + if mr and mr_item_rows: + mr_obj = frappe.get_doc("Material Request", mr) + + if mr_obj.status in ["Stopped", "Cancelled"]: + frappe.throw( + _("Material Request {0} is cancelled or stopped").format(mr), frappe.InvalidStatusError + ) + + mr_obj.update_requested_qty(mr_item_rows) + def get_item_details(items): item = frappe.qb.DocType("Item") diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 8bc38ccb08e..c0064996099 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -13,6 +13,23 @@ from erpnext.stock.utils import get_bin class SubcontractingOrder(SubcontractingController): + def __init__(self, *args, **kwargs): + super(SubcontractingOrder, self).__init__(*args, **kwargs) + + self.status_updater = [ + { + "source_dt": "Subcontracting Order Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "ordered_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_ordered", + "target_ref_field": "stock_qty", + "source_field": "qty", + "percent_join_field": "material_request", + } + ] + def before_validate(self): super(SubcontractingOrder, self).before_validate() @@ -26,11 +43,15 @@ class SubcontractingOrder(SubcontractingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") def on_submit(self): + self.update_prevdoc_status() + self.update_requested_qty() self.update_ordered_qty_for_subcontracting() self.update_reserved_qty_for_subcontracting() self.update_status() def on_cancel(self): + self.update_prevdoc_status() + self.update_requested_qty() self.update_ordered_qty_for_subcontracting() self.update_reserved_qty_for_subcontracting() self.update_status() @@ -164,7 +185,9 @@ class SubcontractingOrder(SubcontractingController): "qty": si.fg_item_qty, "stock_uom": item.stock_uom, "bom": bom, - }, + "material_request": si.material_request, + "material_request_item": si.material_request_item, + } ) else: frappe.throw( diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 34719daabc2..fbdea6ddbee 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -628,6 +628,62 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(ordered_qty + 10, new_ordered_qty) + def test_requested_qty_for_subcontracting_order(self): + from erpnext.stock.doctype.material_request.material_request import make_purchase_order + from erpnext.stock.doctype.material_request.test_material_request import make_material_request + + requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + requested_qty = flt(requested_qty) + + mr = make_material_request( + item_code="Subcontracted Item SA8", + material_request_type="Purchase", + qty=10, + ) + + self.assertTrue(mr.docstatus == 1) + + new_requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + new_requested_qty = flt(new_requested_qty) + + self.assertEqual(requested_qty + 10, new_requested_qty) + + po = make_purchase_order(mr.name) + po.is_subcontracted = 1 + po.supplier = "_Test Supplier" + po.items[0].fg_item = "Subcontracted Item SA8" + po.items[0].fg_item_qty = 10 + po.items[0].item_code = "Subcontracted Service Item 8" + po.items[0].item_name = "Subcontracted Service Item 8" + po.items[0].qty = 10 + po.supplier_warehouse = "_Test Warehouse 1 - _TC" + po.save() + po.submit() + + self.assertTrue(po.items[0].material_request) + self.assertTrue(po.items[0].material_request_item) + + sco = create_subcontracting_order(po_name=po.name) + self.assertTrue(sco.items[0].material_request) + self.assertTrue(sco.items[0].material_request_item) + + new_requested_qty = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"}, + fieldname="indented_qty", + ) + new_requested_qty = flt(new_requested_qty) + + self.assertEqual(requested_qty, new_requested_qty) + def create_subcontracting_order(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index 46c229bfd37..58b4e32a123 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -40,6 +40,11 @@ "manufacture_section", "manufacturer", "manufacturer_part_no", + "column_break_impp", + "reference_section", + "material_request", + "column_break_fpyl", + "material_request_item", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -332,13 +337,44 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "fieldname": "column_break_impp", + "fieldtype": "Column Break" + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "no_copy": 1, + "options": "Material Request", + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item", + "no_copy": 1, + "read_only": 1, + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "column_break_fpyl", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:38:37.640677", + "modified": "2023-11-30 15:29:43.744618", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", @@ -351,4 +387,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json index f213313ef6b..2fdcdab7fde 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json @@ -1,131 +1,160 @@ { - "actions": [], - "autoname": "hash", - "creation": "2022-04-01 19:23:05.728354", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "item_code", - "column_break_2", - "item_name", - "section_break_4", - "qty", - "column_break_6", - "rate", - "column_break_8", - "amount", - "section_break_10", - "fg_item", - "column_break_12", - "fg_item_qty" - ], - "fields": [ - { - "bold": 1, - "columns": 2, - "fieldname": "item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Code", - "options": "Item", - "reqd": 1, - "search_index": 1 - }, - { - "fetch_from": "item_code.item_name", - "fieldname": "item_name", - "fieldtype": "Data", - "in_global_search": 1, - "in_list_view": 1, - "label": "Item Name", - "print_hide": 1, - "reqd": 1 - }, - { - "bold": 1, - "columns": 1, - "fieldname": "qty", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Quantity", - "print_width": "60px", - "reqd": 1, - "width": "60px" - }, - { - "bold": 1, - "columns": 2, - "fetch_from": "item_code.standard_rate", - "fetch_if_empty": 1, - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate", - "options": "currency", - "reqd": 1 - }, - { - "columns": 2, - "fieldname": "amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Amount", - "options": "currency", - "read_only": 1, - "reqd": 1 - }, - { - "fieldname": "fg_item", - "fieldtype": "Link", - "label": "Finished Good Item", - "options": "Item", - "reqd": 1 - }, - { - "default": "1", - "fieldname": "fg_item_qty", - "fieldtype": "Float", - "label": "Finished Good Item Quantity", - "reqd": 1 - }, - { - "fieldname": "column_break_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_4", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_8", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - } - ], - "istable": 1, - "links": [], - "modified": "2022-04-07 11:43:43.094867", - "modified_by": "Administrator", - "module": "Subcontracting", - "name": "Subcontracting Order Service Item", - "naming_rule": "Random", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "search_fields": "item_name", - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file + "actions": [], + "autoname": "hash", + "creation": "2022-04-01 19:23:05.728354", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "section_break_4", + "qty", + "column_break_6", + "rate", + "column_break_8", + "amount", + "section_break_10", + "fg_item", + "column_break_12", + "fg_item_qty", + "section_break_kphn", + "material_request", + "column_break_piqi", + "material_request_item" + ], + "fields": [ + { + "bold": 1, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "bold": 1, + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "60px", + "reqd": 1, + "width": "60px" + }, + { + "bold": 1, + "columns": 2, + "fetch_from": "item_code.standard_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "fg_item", + "fieldtype": "Link", + "label": "Finished Good Item", + "options": "Item", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "fg_item_qty", + "fieldtype": "Float", + "label": "Finished Good Item Quantity", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "section_break_kphn", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "material_request", + "fieldtype": "Link", + "label": "Material Request", + "no_copy": 1, + "options": "Material Request", + "read_only": 1 + }, + { + "fieldname": "column_break_piqi", + "fieldtype": "Column Break" + }, + { + "fieldname": "material_request_item", + "fieldtype": "Data", + "label": "Material Request Item", + "no_copy": 1, + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2023-11-30 13:29:31.017440", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Service Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "item_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} From 0df52d2c4f900e2110798347eac306c16536f8e7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:32:49 +0530 Subject: [PATCH 17/45] fix: show item name as title instead of item group in BOM (backport #38478) (#38480) fix: show item name as title instead of item group in BOM (#38478) Item fields in BOM used to show Item Group when Items were set to show title as link fields. Now they show Item Name instead (cherry picked from commit 3a66aefd2c6aedc1b7b6017c174261c1ed4c2907) Co-authored-by: Gughan Ravikumar --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8d593d12963..6151af7e3ef 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1306,7 +1306,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): order_by = "idx desc, name, item_name" - fields = ["name", "item_group", "item_name", "description"] + fields = ["name", "item_name", "item_group", "description"] fields.extend( [field for field in searchfields if not field in ["name", "item_group", "description"]] ) From 8d6d74c23700dc22e27e2cfce4af0227a4ab3be3 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 30 Nov 2023 06:20:15 +0000 Subject: [PATCH 18/45] fix(pe): show split alert only on splitting (cherry picked from commit 96b13c59c13350acdf0bef8374a587cd572c00ae) --- .../accounts/doctype/payment_entry/payment_entry.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 778a441b237..2cfd2115874 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1566,12 +1566,13 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list if not split_rows: continue - frappe.msgprint( - _("Splitting {0} {1} into {2} rows as per Payment Terms").format( - _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) - ), - alert=True, - ) + if len(split_rows) > 1: + frappe.msgprint( + _("Splitting {0} {1} into {2} rows as per Payment Terms").format( + _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) + ), + alert=True, + ) outstanding_invoices_after_split += split_rows continue From b813964ee3a3b209954c61b113ec3079a43bf184 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:09:20 +0530 Subject: [PATCH 19/45] chore: changed sort_order to DESC for customer (backport #38498) (#38500) chore: changed sort_order to DESC for customer (#38498) (cherry picked from commit 6bc40373f234af10ce4490529e96c5348d27b7df) Co-authored-by: Sherin KR --- erpnext/selling/doctype/customer/customer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 87e5c5d0eec..c3c7766b9f5 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -651,7 +651,7 @@ "search_fields": "customer_name,customer_group,territory, mobile_no,primary_address", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC", + "sort_order": "DESC", "states": [], "title_field": "customer_name", "track_changes": 1 From 9345334196484f3d89c1be11bcb0598cf38cfab7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 2 Dec 2023 20:06:48 +0530 Subject: [PATCH 20/45] fix: don't consider cancelled entries (#38401) * fix: don't consider cancelled entries (cherry picked from commit adfcdb3b652aa98db96b466371788f28ab8d26b7) * refactor: get outstanding journal entry using query builder (cherry picked from commit ff27cccff42f9f4cd54aa6071a71687d5183091a) --------- Co-authored-by: Devin Slauenwhite --- .../doctype/payment_entry/payment_entry.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2cfd2115874..a3ebf045fc9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -9,6 +9,8 @@ import frappe from frappe import ValidationError, _, qb, scrub, throw from frappe.utils import cint, comma_or, flt, getdate, nowdate from frappe.utils.data import comma_and, fmt_money +from pypika import Case +from pypika.functions import Coalesce, Sum import erpnext from erpnext.accounts.doctype.bank_account.bank_account import ( @@ -1854,18 +1856,24 @@ def get_company_defaults(company): def get_outstanding_on_journal_entry(name): - res = frappe.db.sql( - "SELECT " - 'CASE WHEN party_type IN ("Customer") ' - "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) " - "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) " - "END as outstanding_amount " - "FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) " - "AND party_type IS NOT NULL " - 'AND party_type != ""', - (name, name), - as_dict=1, - ) + gl = frappe.qb.DocType("GL Entry") + res = ( + frappe.qb.from_(gl) + .select( + Case() + .when( + gl.party_type == "Customer", + Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0), + ) + .else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0)) + .as_("outstanding_amount") + ) + .where( + (Coalesce(gl.party_type, "") != "") + & (gl.is_cancelled == 0) + & ((gl.voucher_no == name) | (gl.against_voucher == name)) + ) + ).run(as_dict=True) outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0 From b88c7d63deebb331f49512191565f359221b75e7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:02:59 +0530 Subject: [PATCH 21/45] fix: don't update previous doc on rate change (backport #38493) (#38523) fix: don't update previous doc on rate change (cherry picked from commit 68f5dd3e7b836c71f73cef210fcb4d72dc58e557) Co-authored-by: s-aga-r --- erpnext/controllers/accounts_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4ef251f8add..bf792c5e896 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3129,7 +3129,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent_doctype == "Purchase Order": update_last_purchase_rate(parent, is_submit=1) - parent.update_prevdoc_status() + + if any_qty_changed or items_added_or_removed or any_conversion_factor_changed: + parent.update_prevdoc_status() + parent.update_requested_qty() parent.update_ordered_qty() parent.update_ordered_and_reserved_qty() From 6db63c971eb88ad68b916c1947e81806c0440638 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Dec 2023 15:15:30 +0530 Subject: [PATCH 22/45] fix: better overlap logic for job card (backport #38432) (#38521) * fix: better overlap logic for job card (#38432) (cherry picked from commit 74eab910427ca4af94119b0c099e7743be770006) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py * chore: fix conflicts * chore: fixed existing_time_logs not defined --------- Co-authored-by: rohitwaghchaure --- .../doctype/job_card/job_card.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a5779fb299c..455aa7e5766 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -213,29 +213,27 @@ class JobCard(Document): production_capacity = 1 query = query.where(jctl.employee == args.get("employee")) - existing = query.run(as_dict=True) + existing_time_logs = query.run(as_dict=True) - overlap_count = self.get_overlap_count(existing) - if existing and production_capacity > overlap_count: - return + if not self.has_overlap(production_capacity, existing_time_logs): + return {} if self.workstation_type: - if workstation := self.get_workstation_based_on_available_slot(existing): + if workstation := self.get_workstation_based_on_available_slot(existing_time_logs): self.workstation = workstation return None - return existing[0] if existing else None + return existing_time_logs[0] if existing_time_logs else None - @staticmethod - def get_overlap_count(time_logs): - count = 1 + def has_overlap(self, production_capacity, time_logs): + overlap = False + if production_capacity == 1 and len(time_logs) > 0: + return True # Check overlap exists or not between the overlapping time logs with the current Job Card - for idx, row in enumerate(time_logs): - next_idx = idx - if idx + 1 < len(time_logs): - next_idx = idx + 1 - next_row = time_logs[next_idx] + for row in time_logs: + count = 1 + for next_row in time_logs: if row.name == next_row.name: continue @@ -255,7 +253,10 @@ class JobCard(Document): ): count += 1 - return count + if count > production_capacity: + return True + + return overlap def get_workstation_based_on_available_slot(self, existing) -> Optional[str]: workstations = get_workstations(self.workstation_type) From eee906281480fbed66e3ad823bd4e0386ed07bd3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 3 Dec 2023 10:33:31 +0530 Subject: [PATCH 23/45] fix: item group filter in sales person wise report (cherry picked from commit f4d418ea6d28f64d6d1c15c5eea2a75722384c0c) --- .../sales_person_wise_transaction_summary.py | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index cb6e8a1102f..9f3ba0da8bd 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -3,7 +3,8 @@ import frappe -from frappe import _, msgprint +from frappe import _, msgprint, qb +from frappe.query_builder import Criterion from erpnext import get_company_currency @@ -214,24 +215,33 @@ def get_conditions(filters, date_field): if items: conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items))) values += items + else: + # return empty result, if no items are fetched after filtering on 'item group' and 'brand' + conditions.append("dt_item.item_code = Null") return " and ".join(conditions), values def get_items(filters): + item = qb.DocType("Item") + + item_query_conditions = [] if filters.get("item_group"): - key = "item_group" - elif filters.get("brand"): - key = "brand" - else: - key = "" - - items = [] - if key: - items = frappe.db.sql_list( - """select name from tabItem where %s = %s""" % (key, "%s"), (filters[key]) + # Handle 'Parent' nodes as well. + item_group = qb.DocType("Item Group") + lft, rgt = frappe.db.get_all( + "Item Group", filters={"name": filters.get("item_group")}, fields=["lft", "rgt"], as_list=True + )[0] + item_group_query = ( + qb.from_(item_group) + .select(item_group.name) + .where((item_group.lft >= lft) & (item_group.rgt <= rgt)) ) + item_query_conditions.append(item.item_group.isin(item_group_query)) + if filters.get("brand"): + item_query_conditions.append(item.brand == filters.get("brand")) + items = qb.from_(item).select(item.name).where(Criterion.all(item_query_conditions)).run() return items From 419943a9d9ea748088702935844ed616f616400a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 3 Dec 2023 10:50:18 +0530 Subject: [PATCH 24/45] fix: remove hardcoded, implicit rounding loss allowance (cherry picked from commit 64266c4d3826e19b293f522bfb4cbbb305f817b7) --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 3b5698b118a..977cfe94f8a 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -192,7 +192,7 @@ class ExchangeRateRevaluation(Document): # round off balance based on currency precision # and consider debit-credit difference allowance currency_precision = get_currency_precision() - rounding_loss_allowance = float(rounding_loss_allowance) or 0.05 + rounding_loss_allowance = float(rounding_loss_allowance) for acc in account_details: acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision) if abs(acc.balance_in_account_currency) <= rounding_loss_allowance: From b7fe163de5ece72a91862695537274c6aeb7eb06 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:35:51 +0530 Subject: [PATCH 25/45] fix: SO ordered qty on PO item removal (#38378) * fix: SO ordered qty on PO item removal (#38378) * fix: update ordered_qty for SO when PO items removed * refactor: use cached value --------- Co-authored-by: Deepesh Garg (cherry picked from commit 9087e1443e4719ef07888ba43b167fe24bd352e1) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.py * chore: resolve conflicts --------- Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> --- .../doctype/purchase_order/purchase_order.py | 14 ++++++++++++++ erpnext/controllers/accounts_controller.py | 3 +++ 2 files changed, 17 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 51b3a4771a0..c1c813db044 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -441,6 +441,20 @@ class PurchaseOrder(BuyingController): else: self.db_set("per_received", 0, update_modified=False) + def update_ordered_qty_in_so_for_removed_items(self, removed_items): + """ + Updates ordered_qty in linked SO when item rows are removed using Update Items + """ + if not self.is_against_so(): + return + for item in removed_items: + prev_ordered_qty = frappe.get_cached_value( + "Sales Order Item", item.get("sales_order_item"), "ordered_qty" + ) + frappe.db.set_value( + "Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty + ) + def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bf792c5e896..393ad171d52 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2874,6 +2874,9 @@ def validate_and_delete_children(parent, data) -> bool: d.cancel() d.delete() + if parent.doctype == "Purchase Order": + parent.update_ordered_qty_in_so_for_removed_items(deleted_children) + # need to update ordered qty in Material Request first # bin uses Material Request Items to recalculate & update parent.update_prevdoc_status() From 9f23b7e46f52390317b13b29ca5ddbe9842effe1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:52:16 +0530 Subject: [PATCH 26/45] fix: use predefined onload property `load_after_mapping` (backport #38209) (#38518) Co-authored-by: Sagar Vora fix: use predefined onload property `load_after_mapping` (#38209) --- .../doctype/purchase_invoice/purchase_invoice.py | 2 -- .../doctype/sales_invoice/sales_invoice.py | 1 - .../doctype/purchase_order/purchase_order.py | 3 --- .../supplier_quotation/supplier_quotation.py | 1 - erpnext/controllers/sales_and_purchase_return.py | 2 -- erpnext/public/js/controllers/transaction.js | 14 +++++++------- erpnext/selling/doctype/quotation/quotation.py | 5 ----- erpnext/selling/doctype/sales_order/sales_order.py | 4 ---- .../stock/doctype/delivery_note/delivery_note.py | 2 -- .../doctype/purchase_receipt/purchase_receipt.py | 1 - 10 files changed, 7 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 8bbea96e1d2..7a7ff9b5ee5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1703,6 +1703,4 @@ def make_purchase_receipt(source_name, target_doc=None): target_doc, ) - doc.set_onload("ignore_price_list", True) - return doc diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 34ea24dde49..434662c298b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1934,7 +1934,6 @@ def make_delivery_note(source_name, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) return doclist diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index c1c813db044..8131104b825 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -541,8 +541,6 @@ def make_purchase_receipt(source_name, target_doc=None): set_missing_values, ) - doc.set_onload("ignore_price_list", True) - return doc @@ -622,7 +620,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions postprocess, ignore_permissions=ignore_permissions, ) - doc.set_onload("ignore_price_list", True) return doc diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 2dd748bc19c..e27fbe8aaa2 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -168,7 +168,6 @@ def make_purchase_order(source_name, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) return doclist diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5a6c87c2169..efeedc14db6 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -530,8 +530,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) - return doclist diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index fe24b18098a..050b9dcd3db 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -328,7 +328,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe onload_post_render() { if(this.frm.doc.__islocal && !(this.frm.doc.taxes || []).length - && !(this.frm.doc.__onload ? this.frm.doc.__onload.load_after_mapping : false)) { + && !this.frm.doc.__onload?.load_after_mapping) { frappe.after_ajax(() => this.apply_default_taxes()); } else if(this.frm.doc.__islocal && this.frm.doc.company && this.frm.doc["items"] && !this.frm.doc.is_pos) { @@ -918,9 +918,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let me = this; this.set_dynamic_labels(); let company_currency = this.get_company_currency(); - // Added `ignore_price_list` to determine if document is loading after mapping from another doc + // Added `load_after_mapping` to determine if document is loading after mapping from another doc if(this.frm.doc.currency && this.frm.doc.currency !== company_currency - && !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) { + && !this.frm.doc.__onload?.load_after_mapping) { this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency, function(exchange_rate) { @@ -952,7 +952,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } if(flt(this.frm.doc.conversion_rate)>0.0) { - if(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { + if(this.frm.doc.__onload?.load_after_mapping) { this.calculate_taxes_and_totals(); } else if (!this.in_apply_price_list){ this.apply_price_list(); @@ -1039,9 +1039,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.set_dynamic_labels(); var company_currency = this.get_company_currency(); - // Added `ignore_price_list` to determine if document is loading after mapping from another doc + // Added `load_after_mapping` to determine if document is loading after mapping from another doc if(this.frm.doc.price_list_currency !== company_currency && - !(this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list)) { + !this.frm.doc.__onload?.load_after_mapping) { this.get_exchange_rate(this.frm.doc.posting_date, this.frm.doc.price_list_currency, company_currency, function(exchange_rate) { me.frm.set_value("plc_conversion_rate", exchange_rate); @@ -1420,7 +1420,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } // Target doc created from a mapped doc - if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { + if (this.frm.doc.__onload?.load_after_mapping) { // Calculate totals even though pricing rule is not applied. // `apply_pricing_rule` is triggered due to change in data which most likely contributes to Total. if (calculate_taxes_and_totals) me.calculate_taxes_and_totals(); diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 8ff681b0481..88e24ad542a 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -352,9 +352,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ignore_permissions=ignore_permissions, ) - # postprocess: fetch shipping address, set missing values - doclist.set_onload("ignore_price_list", True) - return doclist @@ -423,8 +420,6 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): ignore_permissions=ignore_permissions, ) - doclist.set_onload("ignore_price_list", True) - return doclist diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index c7c7e526f4c..cbc11ed02af 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -716,8 +716,6 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) - target_doc.set_onload("ignore_price_list", True) - return target_doc @@ -806,8 +804,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if automatically_fetch_payment_terms: doclist.set_payment_schedule() - doclist.set_onload("ignore_price_list", True) - return doclist diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 571e0ea5d7a..d9d9a52482a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -665,8 +665,6 @@ def make_sales_invoice(source_name, target_doc=None): if automatically_fetch_payment_terms: doc.set_payment_schedule() - doc.set_onload("ignore_price_list", True) - return doc diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index bb3cbd2fa6a..c12048077e7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1057,7 +1057,6 @@ def make_purchase_invoice(source_name, target_doc=None): set_missing_values, ) - doclist.set_onload("ignore_price_list", True) return doclist From 1042d4cfeb158a224ea722816fec346a940f2c98 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 20 Nov 2023 11:10:56 +0000 Subject: [PATCH 27/45] fix: exclude `invoice_doctypes` from party advance (cherry picked from commit f34ffc20620de442b1a0c9f3d0eb4eb0ff689831) --- erpnext/accounts/party.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 037f0ec4a15..c24d28b3249 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -195,7 +195,7 @@ def set_address_details( company_address=None, shipping_address=None, *, - ignore_permissions=False + ignore_permissions=False, ): billing_address_field = ( "customer_address" if party_type == "Lead" else party_type.lower() + "_address" @@ -239,7 +239,7 @@ def set_address_details( shipping_address_display=render_address( shipping_address, check_permissions=not ignore_permissions ), - **get_fetch_values(doctype, "shipping_address", shipping_address) + **get_fetch_values(doctype, "shipping_address", shipping_address), ) if party_details.company_address: @@ -250,7 +250,7 @@ def set_address_details( party_details.company_address_display or render_address(party_details.company_address, check_permissions=False) ), - **get_fetch_values(doctype, "billing_address", party_details.company_address) + **get_fetch_values(doctype, "billing_address", party_details.company_address), ) # shipping address - if not already set @@ -258,7 +258,7 @@ def set_address_details( party_details.update( shipping_address=party_details.billing_address, shipping_address_display=party_details.billing_address_display, - **get_fetch_values(doctype, "shipping_address", party_details.billing_address) + **get_fetch_values(doctype, "shipping_address", party_details.billing_address), ) party_address, shipping_address = ( @@ -956,6 +956,9 @@ def get_partywise_advanced_payment_amount( if party: query = query.where(ple.party == party) + if invoice_doctypes := frappe.get_hooks("invoice_doctypes"): + query = query.where(ple.voucher_type.notin(invoice_doctypes)) + data = query.run() if data: return frappe._dict(data) From 36dc7bd55e3d3284eabaa13e79d0ecf41bb45d3f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 17 Nov 2023 17:34:21 +0530 Subject: [PATCH 28/45] refactor: convert payment reconciliation tool to virtual doctype (cherry picked from commit 3a51a3f37ecd70c13cee558a0b882d06e812a21c) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json --- .../payment_reconciliation/payment_reconciliation.json | 8 +++++++- .../payment_reconciliation_allocation.json | 3 ++- .../payment_reconciliation_invoice.json | 3 ++- .../payment_reconciliation_payment.json | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 0dc9c135b8c..19b146494c1 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -203,9 +203,10 @@ ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", + "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-08-15 05:35:50.109290", + "modified": "2023-11-17 17:33:55.701726", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -230,6 +231,11 @@ ], "sort_field": "modified", "sort_order": "DESC", +<<<<<<< HEAD "states": [], "track_changes": 1 } +======= + "states": [] +} +>>>>>>> 3a51a3f37e (refactor: convert payment reconciliation tool to virtual doctype) diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 5b8556e7c83..491c67818df 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -159,9 +159,10 @@ "label": "Difference Posting Date" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-10-23 10:44:56.066303", + "modified": "2023-11-17 17:33:38.612615", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index c4dbd7e8441..7c9d49e7731 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -71,9 +71,10 @@ "label": "Exchange Rate" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2022-11-08 18:18:02.502149", + "modified": "2023-11-17 17:33:45.455166", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index 17f3900880c..d199236ae99 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -107,9 +107,10 @@ "options": "Cost Center" } ], + "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-09-03 07:43:29.965353", + "modified": "2023-11-17 17:33:34.818530", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", From c349c89ae31505cd2993c7512356b4f8ef069b7c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 18 Nov 2023 07:48:15 +0530 Subject: [PATCH 29/45] refactor: virtual doctype methods (cherry picked from commit 9c7b19e0b7732189c9db12e2c67b12a67fe562b3) --- .../payment_reconciliation.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index d2c75111edd..a907dc7acf5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -29,6 +29,58 @@ class PaymentReconciliation(Document): self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] + def load_from_db(self): + # 'modified' attribute is required for `run_doc_method` to work properly. + doc_dict = frappe._dict( + { + "modified": None, + "company": None, + "party": None, + "party_type": None, + "receivable_payable_account": None, + "default_advance_account": None, + "from_invoice_date": None, + "to_invoice_date": None, + "invoice_limit": 50, + "from_payment_date": None, + "to_payment_date": None, + "payment_limit": 50, + "minimum_invoice_amount": None, + "minimum_payment_amount": None, + "maximum_invoice_amount": None, + "maximum_payment_amount": None, + "bank_cash_account": None, + "cost_center": None, + "payment_name": None, + "invoice_name": None, + } + ) + super(Document, self).__init__(doc_dict) + + def save(self): + return + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + + def db_insert(self, *args, **kwargs): + pass + + def db_update(self, *args, **kwargs): + pass + + def delete(self): + pass + @frappe.whitelist() def get_unreconciled_entries(self): self.get_nonreconciled_payment_entries() From 454d789232ebcdc1b508c6b14d0d7d30c721361b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 18 Nov 2023 08:07:36 +0530 Subject: [PATCH 30/45] chore: remove reconciliation defaults from patch (cherry picked from commit b5dd0c8630c59cc38011039e7c7a21976b53ebd8) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b8cda748509..56aca635a8f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -344,10 +344,13 @@ erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field +<<<<<<< HEAD execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based +======= +>>>>>>> b5dd0c8630 (chore: remove reconciliation defaults from patch) erpnext.patches.v14_0.add_default_for_repost_settings erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field From 59773acdd2d43e840b6a6ccb3b6ffca602e93af2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 18 Nov 2023 08:10:09 +0530 Subject: [PATCH 31/45] chore: clear singles table and reconciliation related tables (cherry picked from commit f31002636bf21a3599ee5a7372db4d0cb3dbfad9) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 7 +++++++ .../clear_reconciliation_values_from_singles.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 56aca635a8f..bf3c6291c4a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,8 +352,15 @@ erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_d ======= >>>>>>> b5dd0c8630 (chore: remove reconciliation defaults from patch) erpnext.patches.v14_0.add_default_for_repost_settings +<<<<<<< HEAD erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") +======= +erpnext.patches.v14_0.clear_reconciliation_values_from_singles +erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month +erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based +erpnext.patches.v15_0.set_reserved_stock_in_bin +>>>>>>> f31002636b (chore: clear singles table and reconciliation related tables) # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py new file mode 100644 index 00000000000..c1f5b60a406 --- /dev/null +++ b/erpnext/patches/v14_0/clear_reconciliation_values_from_singles.py @@ -0,0 +1,17 @@ +from frappe import qb + + +def execute(): + """ + Clear `tabSingles` and Payment Reconciliation tables of values + """ + singles = qb.DocType("Singles") + qb.from_(singles).delete().where(singles.doctype == "Payment Reconciliation").run() + doctypes = [ + "Payment Reconciliation Invoice", + "Payment Reconciliation Payment", + "Payment Reconciliation Allocation", + ] + for x in doctypes: + dt = qb.DocType(x) + qb.from_(dt).delete().run() From 2e333e6802ad880700920e5df70b87495a67a657 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:18:31 +0530 Subject: [PATCH 32/45] fix: don't show non-stock items in Stock Analytics report (backport #38543) (#38544) * fix(ux): stock-item filter for Item Code field (cherry picked from commit ccdcb7dfcc29eb68d1cad38f3426e6fb575528eb) * fix: don't show non-stock items in Stock Analytics report (cherry picked from commit 01aadbef85e2570399f054027e57d6bb01af9433) * fix: `linter` (cherry picked from commit 15fff84bb5cc78507aa24771fde3e9a9fbb926bc) --------- Co-authored-by: s-aga-r --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- erpnext/stock/report/stock_analytics/stock_analytics.js | 1 + erpnext/stock/report/stock_analytics/stock_analytics.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index ae3fa875e84..f1abc1d4ddb 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1085,7 +1085,7 @@ class ReceivablePayableReport(object): ) if self.filters.show_remarks: - self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200), + self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200) def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): if not fieldname: diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.js b/erpnext/stock/report/stock_analytics/stock_analytics.js index 78afe6d2642..071bfa22959 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.js +++ b/erpnext/stock/report/stock_analytics/stock_analytics.js @@ -17,6 +17,7 @@ frappe.query_reports["Stock Analytics"] = { fieldtype: "Link", options:"Item", default: "", + get_query: () => ({filters: { 'is_stock_item': 1 }}), }, { fieldname: "value_quantity", diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 6c5b58c6e45..ab48181c48d 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -270,7 +270,7 @@ def get_items(filters): if item_code := filters.get("item_code"): return [item_code] else: - item_filters = {} + item_filters = {"is_stock_item": 1} if item_group := filters.get("item_group"): children = get_descendants_of("Item Group", item_group, ignore_permissions=True) item_filters["item_group"] = ("in", children + [item_group]) From d27e588672fb854c21502f5c81f91b19f6119afc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Dec 2023 12:17:28 +0530 Subject: [PATCH 33/45] chore: resolve conflicts --- .../payment_reconciliation.json | 6 ------ erpnext/patches.txt | 11 ----------- 2 files changed, 17 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 19b146494c1..e8985de4e1e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -231,11 +231,5 @@ ], "sort_field": "modified", "sort_order": "DESC", -<<<<<<< HEAD - "states": [], - "track_changes": 1 -} -======= "states": [] } ->>>>>>> 3a51a3f37e (refactor: convert payment reconciliation tool to virtual doctype) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bf3c6291c4a..94e3569abcf 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -344,23 +344,12 @@ erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item erpnext.patches.v14_0.rename_over_order_allowance_field erpnext.patches.v14_0.migrate_delivery_stop_lock_field -<<<<<<< HEAD -execute:frappe.db.set_single_value("Payment Reconciliation", "invoice_limit", 50) -execute:frappe.db.set_single_value("Payment Reconciliation", "payment_limit", 50) erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based -======= ->>>>>>> b5dd0c8630 (chore: remove reconciliation defaults from patch) erpnext.patches.v14_0.add_default_for_repost_settings -<<<<<<< HEAD erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") -======= erpnext.patches.v14_0.clear_reconciliation_values_from_singles -erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month -erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based -erpnext.patches.v15_0.set_reserved_stock_in_bin ->>>>>>> f31002636b (chore: clear singles table and reconciliation related tables) # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger From 7551ba3851abc560980e6234bd8d33069c94a2ea Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 23 Nov 2023 10:39:41 +0530 Subject: [PATCH 34/45] chore: move reconciliation cleanup patch to pre-model (cherry picked from commit f258ab5e98f323303276aeca98dcc644f6611a70) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 94e3569abcf..624b3d3a330 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -269,7 +269,15 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v14_0.update_reference_due_date_in_journal_entry +<<<<<<< HEAD erpnext.patches.v14_0.france_depreciation_warning +======= +erpnext.patches.v15_0.saudi_depreciation_warning +erpnext.patches.v15_0.delete_saudi_doctypes +erpnext.patches.v14_0.show_loan_management_deprecation_warning +erpnext.patches.v14_0.clear_reconciliation_values_from_singles +execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True) +>>>>>>> f258ab5e98 (chore: move reconciliation cleanup patch to pre-model) [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') @@ -347,6 +355,12 @@ erpnext.patches.v14_0.migrate_delivery_stop_lock_field erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based erpnext.patches.v14_0.add_default_for_repost_settings +<<<<<<< HEAD +======= +erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month +erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based +erpnext.patches.v15_0.set_reserved_stock_in_bin +>>>>>>> f258ab5e98 (chore: move reconciliation cleanup patch to pre-model) erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") From c07723d49d065a29e9b6985c746c82cce1815d6d Mon Sep 17 00:00:00 2001 From: NIYAZ RAZAK <76736615+niyazrazak@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:09:49 +0300 Subject: [PATCH 35/45] fix: incorrect customer outstanding amount (#38475) * fix: incorrect customer outstanding amount * chore: resolve linting error Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> (cherry picked from commit 3df1d75bdd7a34c9fef4c4fe9af9341bc2e6e094) --- erpnext/selling/doctype/customer/customer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 20d6268d739..de3c21e6eb8 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -582,7 +582,8 @@ def get_customer_outstanding( """ select sum(debit) - sum(credit) from `tabGL Entry` where party_type = 'Customer' - and party = %s and company=%s {0}""".format( + and is_cancelled = 0 and party = %s + and company=%s {0}""".format( cond ), (customer, company), From 2e3a860a469b605a5e2c7bfe556c4ed41ce3cb1a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Dec 2023 13:43:41 +0530 Subject: [PATCH 36/45] chore: resolve conflict --- erpnext/patches.txt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 624b3d3a330..279610e56c6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -269,15 +269,8 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v14_0.update_reference_due_date_in_journal_entry -<<<<<<< HEAD erpnext.patches.v14_0.france_depreciation_warning -======= -erpnext.patches.v15_0.saudi_depreciation_warning -erpnext.patches.v15_0.delete_saudi_doctypes -erpnext.patches.v14_0.show_loan_management_deprecation_warning erpnext.patches.v14_0.clear_reconciliation_values_from_singles -execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True) ->>>>>>> f258ab5e98 (chore: move reconciliation cleanup patch to pre-model) [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') @@ -355,12 +348,6 @@ erpnext.patches.v14_0.migrate_delivery_stop_lock_field erpnext.patches.v14_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month erpnext.patches.v14_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based erpnext.patches.v14_0.add_default_for_repost_settings -<<<<<<< HEAD -======= -erpnext.patches.v15_0.rename_daily_depreciation_to_depreciation_amount_based_on_num_days_in_month -erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_daily_prorata_based -erpnext.patches.v15_0.set_reserved_stock_in_bin ->>>>>>> f258ab5e98 (chore: move reconciliation cleanup patch to pre-model) erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") From 12d3cda2d9bf2d8b5fd1c8b74db3011ecace487a Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 4 Dec 2023 19:19:09 +0530 Subject: [PATCH 37/45] fix: get dynamic link with parenttype contact (cherry picked from commit 3d7ad71b22578e46366639ef19de8d665a922c04) --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index deddd17234e..2d5b3573ae4 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -40,7 +40,7 @@ class Lead(SellingController, CRMNote): if self.source == "Existing Customer" and self.customer: contact = frappe.db.get_value( "Dynamic Link", - {"link_doctype": "Customer", "link_name": self.customer}, + {"link_doctype": "Customer", "parenttype": "Contact", "link_name": self.customer}, "parent", ) if contact: From 35c06fabc58c02f5edf69f3b02bc16fed81dae68 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Dec 2023 10:14:05 +0530 Subject: [PATCH 38/45] refactor: ignore unreconcile doc on PI cancel/delete (cherry picked from commit 4ca84eadb607263f8432698ff61c4acfd2f0363b) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 7f0f04ea651..c2aa1d936e5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7a7ff9b5ee5..944a8fb2364 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1290,6 +1290,8 @@ class PurchaseInvoice(BuyingController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payment", + "Unreconcile Payment Entries", "Payment Ledger Entry", "Tax Withheld Vouchers", ) From 6c89f351f46c7d1ba1d0d5129b0f59e093d91a59 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:33:14 +0530 Subject: [PATCH 39/45] feat: `Company` filter in `Stock Ledger Variance` report (backport #38553) (#38573) feat: `Company` filter in `Stock Ledger Variance` report (cherry picked from commit fb3421fccee518799a36a27d79bf4f6632736f81) Co-authored-by: s-aga-r --- .../stock_ledger_variance.js | 16 ++++++++++++---- .../stock_ledger_variance.py | 7 ++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js index b1e4a74571e..bf3a397feef 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js @@ -13,10 +13,18 @@ const DIFFERENCE_FIELD_NAMES = [ frappe.query_reports["Stock Ledger Variance"] = { "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, { "fieldname": "item_code", "fieldtype": "Link", - "label": "Item", + "label": __("Item"), "options": "Item", get_query: function() { return { @@ -27,7 +35,7 @@ frappe.query_reports["Stock Ledger Variance"] = { { "fieldname": "warehouse", "fieldtype": "Link", - "label": "Warehouse", + "label": __("Warehouse"), "options": "Warehouse", get_query: function() { return { @@ -38,7 +46,7 @@ frappe.query_reports["Stock Ledger Variance"] = { { "fieldname": "difference_in", "fieldtype": "Select", - "label": "Difference In", + "label": __("Difference In"), "options": [ "", "Qty", @@ -49,7 +57,7 @@ frappe.query_reports["Stock Ledger Variance"] = { { "fieldname": "include_disabled", "fieldtype": "Check", - "label": "Include Disabled", + "label": __("Include Disabled"), } ], diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 732f108ac41..acbbe9039ac 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -230,7 +230,12 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict: bin.item_code, bin.warehouse, ) - .where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0)) + .where( + (item.is_stock_item == 1) + & (item.has_serial_no == 0) + & (warehouse.is_group == 0) + & (warehouse.company == filters.company) + ) ) if filters.item_code: From 4544727654f841eb6cf7553407e61ce178891fa9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Dec 2023 11:33:16 +0530 Subject: [PATCH 40/45] refactor: ingore on JE cancel as well (cherry picked from commit 1a5d56977e158d27210a2bfeeaa9075cb7094875) --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 2 +- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index cf1341b9c70..c59643280e8 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"]; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 4bf9501e5a0..82a9c0f1524 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -98,6 +98,8 @@ class JournalEntry(AccountsController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payment", + "Unreconcile Payment Entries", ) self.make_gl_entries(1) self.update_advance_paid() From 9f43e9e6689b174d62b1c63361c6cb7709d5440f Mon Sep 17 00:00:00 2001 From: Karol Walczysko Date: Tue, 5 Dec 2023 09:52:23 +0100 Subject: [PATCH 41/45] Fix: Asset JS fieldname depreciation_schedule to schedules in make_schedules_editable function. (#38583) Fix fieldname depreciation_schedule to schedules Renames the depreciation_schedule fieldname to schedules in the make_schedules_editable function. --- erpnext/assets/doctype/asset/asset.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index afe532d345d..36554aab628 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -328,10 +328,10 @@ frappe.ui.form.on('Asset', { var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0 ? true : false; - frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable); + frm.toggle_enable("schedules", is_manual_hence_editable || is_shift_hence_editable); + frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_manual_hence_editable); + frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable); + frm.fields_dict["schedules"].grid.toggle_enable("shift", is_shift_hence_editable); } }, From 70965efd941389b03280a39a41448800e0747a02 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:24:54 +0530 Subject: [PATCH 42/45] fix: incorrect material request quantity in Production Plan (backport #38566) (#38578) fix: incorrect material request quantity in Production Plan (#38566) (cherry picked from commit aaa9036eca4c5d50fb82a346e08d841e1735fc72) Co-authored-by: rohitwaghchaure --- .../production_plan/production_plan.py | 19 ++++--- .../production_plan/test_production_plan.py | 56 ++++++++++++++++++- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8d12ba92103..7778f060146 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1521,19 +1521,23 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): ) locations = get_available_item_locations( - item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True + item.get("item_code"), + warehouses, + item.get("quantity") * item.get("conversion_factor"), + company, + ignore_validation=True, ) required_qty = item.get("quantity") + if item.get("conversion_factor") and item.get("purchase_uom") != item.get("stock_uom"): + # Convert qty to stock UOM + required_qty = required_qty * item.get("conversion_factor") + # get available material by transferring to production warehouse for d in locations: if required_qty <= 0: return - conversion_factor = 1.0 - if purchase_uom != stock_uom and purchase_uom == item["uom"]: - conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) - new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") @@ -1543,10 +1547,11 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): "material_request_type": "Material Transfer", "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM "from_warehouse": d.get("warehouse"), + "conversion_factor": 1.0, } ) - required_qty -= quantity / conversion_factor + required_qty -= quantity new_mr_items.append(new_dict) # raise purchase request for remaining qty @@ -1558,7 +1563,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) - item["quantity"] = required_qty + item["quantity"] = required_qty / item.get("conversion_factor") new_mr_items.append(item) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 4ad7d06c707..6a50a10d07a 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1272,12 +1272,14 @@ class TestProductionPlan(FrappeTestCase): for row in items: row = frappe._dict(row) if row.material_request_type == "Material Transfer": + self.assertTrue(row.uom == row.stock_uom) self.assertTrue(row.from_warehouse in [wh1, wh2]) self.assertEqual(row.quantity, 2) if row.material_request_type == "Purchase": + self.assertTrue(row.uom != row.stock_uom) self.assertTrue(row.warehouse == mrp_warhouse) - self.assertEqual(row.quantity, 12) + self.assertEqual(row.quantity, 12.0) def test_mr_qty_for_same_rm_with_different_sub_assemblies(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom @@ -1393,6 +1395,58 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) + def test_material_request_qty_purchase_and_material_transfer(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + store_warehouse = create_warehouse("Store Warehouse", company="_Test Company") + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_stock_entry( + item_code=bom_item, + qty=60, + target=store_warehouse, + rate=99, + ) + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 10}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1 + ) + + pln.for_warehouse = rm_warehouse + items = get_items_for_material_requests( + pln.as_dict(), warehouses=[{"warehouse": store_warehouse}] + ) + + for row in items: + self.assertEqual(row.get("quantity"), 10.0) + self.assertEqual(row.get("material_request_type"), "Material Transfer") + self.assertEqual(row.get("uom"), "_Test UOM 1") + self.assertEqual(row.get("from_warehouse"), store_warehouse) + self.assertEqual(row.get("conversion_factor"), 1.0) + + items = get_items_for_material_requests( + pln.as_dict(), warehouses=[{"warehouse": pln.for_warehouse}] + ) + + for row in items: + self.assertEqual(row.get("quantity"), 1.0) + self.assertEqual(row.get("material_request_type"), "Purchase") + self.assertEqual(row.get("uom"), "Nos") + self.assertEqual(row.get("conversion_factor"), 10.0) + def create_production_plan(**args): """ From 25ec1fce6105249defd835601e0132735cdd189b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Dec 2023 14:01:04 +0530 Subject: [PATCH 43/45] fix: sql error while filtering on finance book in GL (cherry picked from commit b1d9f3132d69df010e4f8f6e5d24b187823c2d8b) --- erpnext/accounts/report/general_ledger/general_ledger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 2e0a9c5f738..759bb71ab24 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -282,7 +282,8 @@ def get_conditions(filters): if accounting_dimensions: for dimension in accounting_dimensions: - if not dimension.disabled: + # Ignore 'Finance Book' set up as dimension in below logic, as it is already handled in above section + if not dimension.disabled and dimension.document_type != "Finance Book": if filters.get(dimension.fieldname): if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): filters[dimension.fieldname] = get_dimension_with_children( From 14b908f8ab243c94cf1bd0ecde52055dcdbc5e7b Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Tue, 5 Dec 2023 14:44:59 +0530 Subject: [PATCH 44/45] fix: validate finance_books table length in asset (#38584) --- erpnext/assets/doctype/asset/asset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 36554aab628..37fbd184fe1 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -322,7 +322,7 @@ frappe.ui.form.on('Asset', { }, make_schedules_editable: function(frm) { - if (frm.doc.finance_books) { + if (frm.doc.finance_books.length) { var is_manual_hence_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 ? true : false; var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0 From 01b2a1ffebe31f63741e98d4e4c07fc09716546e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:13:59 +0530 Subject: [PATCH 45/45] fix: consider the `Valuation Method` while picking incorrect SLE (backport #38592) (#38594) * fix: incorrect SLE for `Moving Average` valuation method (cherry picked from commit 8beec586709dcd716de4fb46476218baed2ec972) * feat: add `Valuation Method` column in `Stock Ledger Variance` report (cherry picked from commit 16c297c2ecbc9a174e90597756cf46170bbd7c4a) --------- Co-authored-by: s-aga-r --- .../stock_ledger_variance.py | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index acbbe9039ac..189a90aa471 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -55,6 +55,11 @@ def get_columns(): "label": _("Warehouse"), "options": "Warehouse", }, + { + "fieldname": "valuation_method", + "fieldtype": "Data", + "label": _("Valuation Method"), + }, { "fieldname": "voucher_type", "fieldtype": "Link", @@ -194,6 +199,7 @@ def get_columns(): def get_data(filters=None): filters = frappe._dict(filters or {}) item_warehouse_map = get_item_warehouse_combinations(filters) + valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") data = [] if item_warehouse_map: @@ -206,8 +212,17 @@ def get_data(filters=None): continue for row in report_data: - if has_difference(row, precision, filters.difference_in): - data.append(add_item_warehouse_details(row, item_warehouse)) + if has_difference( + row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method + ): + row.update( + { + "item_code": item_warehouse.item_code, + "warehouse": item_warehouse.warehouse, + "valuation_method": item_warehouse.valuation_method or valuation_method, + } + ) + data.append(row) break return data @@ -229,6 +244,7 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict: .select( bin.item_code, bin.warehouse, + item.valuation_method, ) .where( (item.is_stock_item == 1) @@ -248,37 +264,27 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict: return query.run(as_dict=1) -def has_difference(row, precision, difference_in): - has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision) - has_value_difference = ( - flt(row.diff_value_diff, precision) - or flt(row.fifo_value_diff, precision) - or flt(row.fifo_difference_diff, precision) - ) - has_valuation_difference = flt(row.valuation_diff, precision) or flt( - row.fifo_valuation_diff, precision - ) +def has_difference(row, precision, difference_in, valuation_method): + if valuation_method == "Moving Average": + qty_diff = flt(row.difference_in_qty, precision) + value_diff = flt(row.diff_value_diff, precision) + valuation_diff = flt(row.valuation_diff, precision) + else: + qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision) + value_diff = ( + flt(row.diff_value_diff, precision) + or flt(row.fifo_value_diff, precision) + or flt(row.fifo_difference_diff, precision) + ) + valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision) - if difference_in == "Qty" and has_qty_difference: + if difference_in == "Qty" and qty_diff: return True - elif difference_in == "Value" and has_value_difference: + elif difference_in == "Value" and value_diff: return True - elif difference_in == "Valuation" and has_valuation_difference: + elif difference_in == "Valuation" and valuation_diff: return True elif difference_in not in ["Qty", "Value", "Valuation"] and ( - has_qty_difference or has_value_difference or has_valuation_difference + qty_diff or value_diff or valuation_diff ): return True - - return False - - -def add_item_warehouse_details(row, item_warehouse): - row.update( - { - "item_code": item_warehouse.item_code, - "warehouse": item_warehouse.warehouse, - } - ) - - return row