From a3aa4d536ae1abdfe8ec08bad6aa24d166eca5a4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 19 Mar 2023 18:08:15 +0530 Subject: [PATCH 01/18] chore: Update user manual link (#34478) * chore: Update user manual link (#34478) (cherry picked from commit be723bb9d483c615fa0b14b0115338e39e32a698) # Conflicts: # erpnext/patches.txt * chore: resolve conflicts * chore: Update version Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> * chore: Update version Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --------- Co-authored-by: Deepesh Garg Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/patches.txt | 1 + erpnext/patches/v13_0/update_docs_link.py | 14 ++++++++++++++ erpnext/setup/install.py | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/update_docs_link.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ecbf1f8000c..79ec14a28fb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -376,3 +376,4 @@ erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0}) erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v13_0.update_asset_value_for_manual_depr_entries +erpnext.patches.v13_0.update_docs_link diff --git a/erpnext/patches/v13_0/update_docs_link.py b/erpnext/patches/v13_0/update_docs_link.py new file mode 100644 index 00000000000..d6b1c4cffa7 --- /dev/null +++ b/erpnext/patches/v13_0/update_docs_link.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + + +import frappe + + +def execute(): + navbar_settings = frappe.get_single("Navbar Settings") + for item in navbar_settings.help_dropdown: + if item.is_standard and item.route == "https://erpnext.com/docs/user/manual": + item.route = "https://docs.erpnext.com/docs/v13/user/manual/en/introduction" + + navbar_settings.save() diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 20ba74b8cde..634b4c6a72b 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -150,7 +150,7 @@ def add_standard_navbar_items(): { "item_label": "Documentation", "item_type": "Route", - "route": "https://erpnext.com/docs/user/manual", + "route": "https://docs.erpnext.com/docs/v13/user/manual/en/introduction", "is_standard": 1, }, { From d2a1acc2e2662eb7e96df7058f521a00ec2bd654 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 5 Feb 2023 13:09:34 +0530 Subject: [PATCH 02/18] fix: Overallocation of 'qty' from Cr Notes to Parent Invoice Cr Notes 'qty' are overallocated to parent invoice, when there are mulitple instances of same item in Invoice. (cherry picked from commit e2f19c6a14f95d4f26acd8dfa91f50335f58b290) --- erpnext/accounts/report/gross_profit/gross_profit.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index c73cb050f01..738e1055de6 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -465,7 +465,14 @@ class GrossProfitGenerator(object): ): returned_item_rows = self.returned_invoices[row.parent][row.item_code] for returned_item_row in returned_item_rows: - row.qty += flt(returned_item_row.qty) + # returned_items 'qty' should be stateful + if returned_item_row.qty != 0: + if row.qty >= abs(returned_item_row.qty): + row.qty += returned_item_row.qty + returned_item_row.qty = 0 + else: + row.qty = 0 + returned_item_row.qty += row.qty row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) if flt(row.qty) or row.base_amount: From 6eeac48f17c86f2b6bed78eaa58fabc48febeb72 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 14 Mar 2023 16:22:49 +0530 Subject: [PATCH 03/18] refactor: Ignore linked Cr Notes in Report output (cherry picked from commit d0715a82ebfbd691c70c9e01cdf2357f40f19d04) --- .../accounts/report/gross_profit/gross_profit.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 738e1055de6..35213121b32 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -8,6 +8,7 @@ from frappe.query_builder import Order from frappe.utils import cint, flt from erpnext.controllers.queries import get_match_cond +from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.utils import get_incoming_rate @@ -671,6 +672,19 @@ class GrossProfitGenerator(object): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" + conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" + + if self.filters.item_group: + conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) + + if self.filters.sales_person: + conditions += """ + and exists(select 1 + from `tabSales Team` st + where st.parent = `tabSales Invoice`.name + and st.sales_person = %(sales_person)s) + """ + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" From 4e38e8da1bf0abcaf1a5b5a10ef7a855a1bcc0d9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 5 Feb 2023 14:48:29 +0530 Subject: [PATCH 04/18] test: Gross Profit report output for Cr notes 2 New test cases added. 1. Standalone Cr notes will be reported as normal Invoices 2. Cr notes against an Invoice will not overallocate qty if there are multiple instances of same item (cherry picked from commit cc61daeec4fc78f0c50af44db62998d12b2d5ea5) --- .../report/gross_profit/test_gross_profit.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 06a173ee35a..89ed2637e6f 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -380,3 +380,82 @@ class TestGrossProfit(FrappeTestCase): } gp_entry = [x for x in data if x.parent_invoice == sinv.name] self.assertDictContainsSubset(expected_entry, gp_entry[0]) + + def test_crnote_against_invoice_with_multiple_instances_of_same_item(self): + """ + Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items. + """ + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + # Invoice with an item added twice + sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True) + sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False)) + sinv = sinv.save().submit() + + # Create Credit Note for Invoice + cr_note = make_sales_return(sinv.name) + cr_note = cr_note.save().submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 0.0, + "avg._selling_rate": 0.0, + "valuation_rate": 0.0, + "selling_amount": -100.0, + "buying_amount": 0.0, + "gross_profit": -100.0, + "gross_profit_%": 100.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + # Both items of Invoice should have '0' qty + self.assertEqual(len(gp_entry), 2) + self.assertDictContainsSubset(expected_entry, gp_entry[0]) + self.assertDictContainsSubset(expected_entry, gp_entry[1]) + + def test_standalone_cr_notes(self): + """ + Standalone cr notes will be reported as usual + """ + # Make Cr Note + sinv = self.create_sales_invoice( + qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + sinv.is_return = 1 + sinv = sinv.save().submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": -1.0, + "avg._selling_rate": 100.0, + "valuation_rate": 0.0, + "selling_amount": -100.0, + "buying_amount": 0.0, + "gross_profit": -100.0, + "gross_profit_%": 100.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry, gp_entry[0]) From ae88ba5d18dfbf6ff68518be5a66f445d7799f15 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 21 Mar 2023 12:35:44 +0530 Subject: [PATCH 05/18] fix: incorrect depr schedules after asset repair [v13] (#34520) * fix: incorrect schedule after repair for WDV and DD * chore: only fix schedules for assets with calc_depr true * fix: incorrect schedule after repair for straight line and manual --- erpnext/assets/doctype/asset/asset.py | 32 +++++++----- .../doctype/asset_repair/asset_repair.py | 50 ++++++++++++++++--- erpnext/regional/india/utils.py | 21 ++++---- 3 files changed, 74 insertions(+), 29 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d44e2ec0c5d..181309de951 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -387,10 +387,14 @@ class Asset(AccountsController): ) or value_after_depreciation < finance_book.expected_value_after_useful_life ): - depreciation_amount += ( - value_after_depreciation - finance_book.expected_value_after_useful_life - ) - skip_row = True + if ( + not self.flags.increase_in_asset_value_due_to_repair + or not finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance") + ): + depreciation_amount += ( + value_after_depreciation - finance_book.expected_value_after_useful_life + ) + skip_row = True if depreciation_amount > 0: self.append( @@ -1171,17 +1175,21 @@ def get_total_days(date, frequency): @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): - # if the Depreciation Schedule is being prepared for the first time - if not asset.flags.increase_in_asset_life: - depreciation_amount = ( - flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations) - - # if the Depreciation Schedule is being modified after Asset Repair - else: + # 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: depreciation_amount = ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value + elif asset.flags.increase_in_asset_value_due_to_repair: + depreciation_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) + # if the Depreciation Schedule is being prepared for the first time + else: + depreciation_amount = ( + flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) else: depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index cd8fe5b18b7..222e30134ce 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -39,47 +39,61 @@ class AssetRepair(AccountsController): def before_submit(self): self.check_repair_status() + self.asset_doc.flags.increase_in_asset_value_due_to_repair = False + if self.get("stock_consumption") or self.get("capitalize_repair_cost"): + self.asset_doc.flags.increase_in_asset_value_due_to_repair = True + self.increase_asset_value() if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() + calculate_asset_depreciation = frappe.db.get_value( + "Asset", self.asset, "calculate_depreciation" + ) + if self.get("capitalize_repair_cost"): self.make_gl_entries() - if ( - frappe.db.get_value("Asset", self.asset, "calculate_depreciation") - and self.increase_in_asset_life - ): + if calculate_asset_depreciation and self.increase_in_asset_life: self.modify_depreciation_schedule() self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.prepare_depreciation_data() + if calculate_asset_depreciation: + self.update_asset_expected_value_after_useful_life() self.asset_doc.save() def before_cancel(self): self.asset_doc = frappe.get_doc("Asset", self.asset) + self.asset_doc.flags.increase_in_asset_value_due_to_repair = False + if self.get("stock_consumption") or self.get("capitalize_repair_cost"): + self.asset_doc.flags.increase_in_asset_value_due_to_repair = True + self.decrease_asset_value() if self.get("stock_consumption"): self.increase_stock_quantity() + calculate_asset_depreciation = frappe.db.get_value( + "Asset", self.asset, "calculate_depreciation" + ) + if self.get("capitalize_repair_cost"): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.make_gl_entries(cancel=True) - if ( - frappe.db.get_value("Asset", self.asset, "calculate_depreciation") - and self.increase_in_asset_life - ): + if calculate_asset_depreciation and self.increase_in_asset_life: self.revert_depreciation_schedule_on_cancellation() self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.prepare_depreciation_data() + if calculate_asset_depreciation: + self.update_asset_expected_value_after_useful_life() self.asset_doc.save() def after_delete(self): @@ -100,6 +114,26 @@ class AssetRepair(AccountsController): title=_("Missing Warehouse"), ) + def update_asset_expected_value_after_useful_life(self): + for row in self.asset_doc.get("finance_books"): + if row.depreciation_method in ("Written Down Value", "Double Declining Balance"): + accumulated_depreciation_after_full_schedule = [ + d.accumulated_depreciation_amount + for d in self.asset_doc.get("schedules") + if cint(d.finance_book_id) == row.idx + ] + + accumulated_depreciation_after_full_schedule = max( + accumulated_depreciation_after_full_schedule + ) + + asset_value_after_full_schedule = flt( + flt(row.value_after_depreciation) - flt(accumulated_depreciation_after_full_schedule), + row.precision("expected_value_after_useful_life"), + ) + + row.expected_value_after_useful_life = asset_value_after_full_schedule + def increase_asset_value(self): total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 0b61e7faf9b..d5ef3981faf 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1101,18 +1101,21 @@ def update_taxable_values(doc, method): def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): - # if the Depreciation Schedule is being prepared for the first time - if not asset.flags.increase_in_asset_life: - depreciation_amount = ( - flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations) - - # if the Depreciation Schedule is being modified after Asset Repair - else: + # 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: depreciation_amount = ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) - + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value + elif asset.flags.increase_in_asset_value_due_to_repair: + depreciation_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) + # if the Depreciation Schedule is being prepared for the first time + else: + depreciation_amount = ( + flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) else: rate_of_depreciation = row.rate_of_depreciation # if its the first depreciation From 0d5abf1c9522e70928981b0ad310ce7b75bc027f Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 21 Mar 2023 14:30:28 +0530 Subject: [PATCH 06/18] refactor: calc depr in asset repair and if statement (#34526) refactor: minor asset repair of calc depr and if statement --- erpnext/assets/doctype/asset/asset.py | 27 ++++++++++--------- .../doctype/asset_repair/asset_repair.py | 16 +++-------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 181309de951..e46cdb9fc64 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -380,21 +380,24 @@ class Asset(AccountsController): value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if finance_book.expected_value_after_useful_life and ( - ( - n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != finance_book.expected_value_after_useful_life + if ( + finance_book.expected_value_after_useful_life + and ( + ( + n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life + ) + or value_after_depreciation < finance_book.expected_value_after_useful_life ) - or value_after_depreciation < finance_book.expected_value_after_useful_life - ): - if ( + and ( not self.flags.increase_in_asset_value_due_to_repair or not finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance") - ): - depreciation_amount += ( - value_after_depreciation - finance_book.expected_value_after_useful_life - ) - skip_row = True + ) + ): + depreciation_amount += ( + value_after_depreciation - finance_book.expected_value_after_useful_life + ) + skip_row = True if depreciation_amount > 0: self.append( diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 222e30134ce..edcbf3e2fd0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -50,19 +50,15 @@ class AssetRepair(AccountsController): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() - calculate_asset_depreciation = frappe.db.get_value( - "Asset", self.asset, "calculate_depreciation" - ) - if self.get("capitalize_repair_cost"): self.make_gl_entries() - if calculate_asset_depreciation and self.increase_in_asset_life: + if self.asset_doc.calculate_depreciation and self.increase_in_asset_life: self.modify_depreciation_schedule() self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.prepare_depreciation_data() - if calculate_asset_depreciation: + if self.asset_doc.calculate_depreciation: self.update_asset_expected_value_after_useful_life() self.asset_doc.save() @@ -79,20 +75,16 @@ class AssetRepair(AccountsController): if self.get("stock_consumption"): self.increase_stock_quantity() - calculate_asset_depreciation = frappe.db.get_value( - "Asset", self.asset, "calculate_depreciation" - ) - if self.get("capitalize_repair_cost"): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.make_gl_entries(cancel=True) - if calculate_asset_depreciation and self.increase_in_asset_life: + if self.asset_doc.calculate_depreciation and self.increase_in_asset_life: self.revert_depreciation_schedule_on_cancellation() self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.prepare_depreciation_data() - if calculate_asset_depreciation: + if self.asset_doc.calculate_depreciation: self.update_asset_expected_value_after_useful_life() self.asset_doc.save() From 19dda807d1a63034dfc217a5adfee7af6580cf18 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 21 Mar 2023 16:06:10 +0530 Subject: [PATCH 07/18] fix(client): Amount calculation for 0 qty debit notes (#34455) fix(client): Amount calculation for 0 qty debit notes (#34455) fix(client): Amount calculaton for 0 qty debit notes Co-authored-by: Anand Baburajan (cherry picked from commit ee6c107d588ca30e909d9add4a026755eda722de) Co-authored-by: Deepesh Garg --- erpnext/public/js/controllers/taxes_and_totals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 79196c976f8..7b64087102f 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -125,7 +125,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else { // allow for '0' qty on Credit/Debit notes - let qty = item.qty || -1 + let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1; item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); } From 661030aba1a6847b37ac2a7d8ff73f092f697fc1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 21 Mar 2023 16:08:50 +0530 Subject: [PATCH 08/18] fix: german translations (#34312) fix: german translations (#34312) fix: some german translations (cherry picked from commit 59c2e7ec3ee25198a44fb7ac7157b8117eb3e4e6) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/translations/de.csv | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 976c5bf0b7f..552a968f9bb 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -2007,30 +2007,27 @@ Please identify/create Account (Ledger) for type - {0},Bitte identifizieren / er Please login as another user to register on Marketplace,"Bitte melden Sie sich als anderer Benutzer an, um sich auf dem Marktplatz zu registrieren", Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.,"Bitte sicher stellen, dass wirklich alle Transaktionen dieses Unternehmens gelöscht werden sollen. Die Stammdaten bleiben bestehen. Diese Aktion kann nicht rückgängig gemacht werden.", Please mention Basic and HRA component in Company,Bitte erwähnen Sie die Basis- und HRA-Komponente in der Firma, -Please mention Round Off Account in Company,Bitte Abschlusskonto in Unternehmen vermerken, -Please mention Round Off Cost Center in Company,Bitte Abschlusskostenstelle in Unternehmen vermerken, -Please mention no of visits required,"Bitte bei ""Besuche erforderlich"" NEIN angeben", -Please mention the Lead Name in Lead {0},Bitte erwähnen Sie den Lead Name in Lead {0}, -Please pull items from Delivery Note,Bitte Artikel vom Lieferschein nehmen, +Please mention Round Off Account in Company,Bitte ein Standardkonto Konto für Rundungsdifferenzen in Unternehmen einstellen, +Please mention Round Off Cost Center in Company,Bitte eine Kostenstelle für Rundungsdifferenzen in Unternehmen einstellen, +Please mention no of visits required,Bitte die Anzahl der benötigten Wartungsbesuche angeben, +Please pull items from Delivery Note,Bitte Artikel aus dem Lieferschein ziehen, Please register the SIREN number in the company information file,Bitte registrieren Sie die SIREN-Nummer in der Unternehmensinformationsdatei, Please remove this Invoice {0} from C-Form {1},Bitte diese Rechnung {0} vom Kontaktformular {1} entfernen, Please save the patient first,Bitte speichern Sie den Patienten zuerst, Please save the report again to rebuild or update,"Speichern Sie den Bericht erneut, um ihn neu zu erstellen oder zu aktualisieren", "Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Bitte zugewiesenen Betrag, Rechnungsart und Rechnungsnummer in mindestens einer Zeile auswählen", Please select Apply Discount On,"Bitte ""Rabatt anwenden auf"" auswählen", -Please select BOM against item {0},Bitte wählen Sie Stückliste gegen Artikel {0}, -Please select BOM for Item in Row {0},Bitte Stückliste für Artikel in Zeile {0} auswählen, -Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine Stückliste für Artikel {0} auswählen, -Please select Category first,Bitte zuerst Kategorie auswählen, -Please select Charge Type first,Bitte zuerst Chargentyp auswählen, -Please select Company,Bitte Unternehmen auswählen, +Please select BOM against item {0},Bitte eine Stückliste für Artikel {0} auswählen, +Please select BOM for Item in Row {0},Bitte eine Stückliste für den Artikel in Zeile {0} auswählen, +Please select BOM in BOM field for Item {0},Bitte im Stücklistenfeld eine Stückliste für Artikel {0} auswählen, +Please select Category first,Bitte zuerst eine Kategorie auswählen, +Please select Charge Type first,Bitte zuerst einen Chargentyp auswählen, +Please select Company,Bitte ein Unternehmen auswählen, Please select Company and Designation,Bitte wählen Sie Unternehmen und Position, Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten", Please select Company first,Bitte zuerst Unternehmen auswählen, Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert, Please select Completion Date for Completed Repair,Bitte wählen Sie das Abschlussdatum für die abgeschlossene Reparatur, -Please select Course,Bitte wählen Sie Kurs, -Please select Drug,Bitte wählen Sie Arzneimittel, Please select Employee,Bitte wählen Sie Mitarbeiter, Please select Existing Company for creating Chart of Accounts,Bitte wählen Sie Bestehende Unternehmen für die Erstellung von Konten, Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst, @@ -7797,7 +7794,7 @@ Default Employee Advance Account,Standardkonto für Vorschüsse an Arbeitnehmer, Default Cost of Goods Sold Account,Standard-Herstellkosten, Default Income Account,Standard-Ertragskonto, Default Deferred Revenue Account,Standardkonto für passive Rechnungsabgrenzung, -Default Deferred Expense Account,Standard-Rechnungsabgrenzungsposten, +Default Deferred Expense Account,Standardkonto für aktive Rechnungsabgrenzung, Default Payroll Payable Account,Standardkonto für Verbindlichkeiten aus Lohn und Gehalt, Default Expense Claim Payable Account,Standard-Expense Claim Zahlbares Konto, Stock Settings,Lager-Einstellungen, @@ -8865,7 +8862,7 @@ Add Topic to Courses,Hinzufügen eines Themas zu Kursen, This topic is already added to the existing courses,Dieses Thema wurde bereits zu den bestehenden Kursen hinzugefügt, "If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order","Wenn Shopify keinen Kunden in der Bestellung hat, berücksichtigt das System beim Synchronisieren der Bestellungen den Standardkunden für die Bestellung", The accounts are set by the system automatically but do confirm these defaults,"Die Konten werden vom System automatisch festgelegt, bestätigen jedoch diese Standardeinstellungen", -Default Round Off Account,Standard-Rundungskonto, +Default Round Off Account,Standardkonto für Rundungsdifferenzen, Failed Import Log,Importprotokoll fehlgeschlagen, Fixed Error Log,Fehlerprotokoll behoben, Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts,Firma {0} existiert bereits. Durch Fortfahren werden das Unternehmen und der Kontenplan überschrieben, From 00518eb384df35d331633c3860931152fc666734 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 21 Mar 2023 16:16:36 +0530 Subject: [PATCH 09/18] fix: exchange gain/loss GL's should be removed if advance is cancelled (#34529) * fix: report GL for invoice when advance has different exchange rate If deferred revenue/expense is enabled for any item, don't repost. * test: cancelling advance should remove exchange gain/loss If there are no deferred revenue/expense cancelling advance should cancel the exchange gain/loss booked due to different exchange rates of payment and its linked invoice --- .../doctype/payment_entry/payment_entry.py | 25 ++++++- .../sales_invoice/test_sales_invoice.py | 72 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index f4367cdafd6..f0d7d57fc64 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -7,7 +7,7 @@ from functools import reduce import frappe from frappe import ValidationError, _, scrub, throw -from frappe.utils import cint, comma_or, flt, getdate, nowdate +from frappe.utils import cint, comma_or, flt, get_link_to_form, getdate, nowdate from six import iteritems, string_types import erpnext @@ -168,8 +168,31 @@ class PaymentEntry(AccountsController): for reference in self.references: if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"): doc = frappe.get_doc(reference.reference_doctype, reference.reference_name) + + repost_required = False + for adv_reference in doc.get("advances"): + if adv_reference.exchange_gain_loss != 0: + repost_required = True + break + if repost_required: + for item in doc.get("items"): + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): + frappe.msgprint( + _( + "Linked Invoice {0} has Exchange Gain/Loss GL entries due to this Payment. Submit a Journal manually to reverse its effects." + ).format(get_link_to_form(doc.doctype, doc.name)) + ) + repost_required = False + doc.delink_advance_entries(self.name) + if repost_required: + doc.reload() + doc.docstatus = 2 + doc.make_gl_entries() + doc.docstatus = 1 + doc.make_gl_entries() + def set_missing_values(self): if self.payment_type == "Internal Transfer": for field in ( diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6035e86d067..46ffd7e18d0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3470,6 +3470,78 @@ class TestSalesInvoice(unittest.TestCase): "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled ) + def test_gain_loss_on_advance_cancellation(self): + unlink_enabled = frappe.db.get_single_value( + "Accounts Settings", "unlink_payment_on_cancellation_of_invoice" + ) + + frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1) + + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "payment_type": "Receive", + "party_type": "Customer", + "party": "_Test Customer USD", + "company": "_Test Company", + "paid_from_account_currency": "USD", + "paid_to_account_currency": "INR", + "source_exchange_rate": 70, + "target_exchange_rate": 1, + "reference_no": "1", + "reference_date": nowdate(), + "received_amount": 70, + "paid_amount": 1, + "paid_from": "_Test Receivable USD - _TC", + "paid_to": "_Test Cash - _TC", + } + ) + pe.insert() + pe.submit() + + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=75, + do_not_save=1, + rate=1, + ) + si = si.save() + + si.append( + "advances", + { + "reference_type": "Payment Entry", + "reference_name": pe.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 70, + }, + ) + si.save() + si.submit() + expected_gle = [ + ["_Test Receivable USD - _TC", 75.0, 5.0], + ["Exchange Gain/Loss - _TC", 5.0, 0.0], + ["Sales - _TC", 0.0, 75.0], + ] + check_gl_entries(self, si.name, expected_gle, nowdate()) + + # cancel advance payment + pe.reload() + pe.cancel() + + expected_gle_after = [ + ["_Test Receivable USD - _TC", 75.0, 0.0], + ["Sales - _TC", 0.0, 75.0], + ] + check_gl_entries(self, si.name, expected_gle_after, nowdate()) + + frappe.db.set_single_value( + "Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled + ) + def test_batch_expiry_for_sales_invoice_return(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.item.test_item import make_item From 8ddbac515882469ed1ec19124e39f656c6581311 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 22 Mar 2023 13:50:29 +0530 Subject: [PATCH 10/18] fix: `Blanket Order` (backport #34279) (#34548) * fix: hide `+` button based on `Blanket Order Type` (cherry picked from commit abf9a28d6af8b3c9bfab1e892e56bf3adb18ee8e) * feat: add field `Over Order Allowance (%)` in `Buying Settings` (cherry picked from commit f5937f46cb60f3521463f7a4c80c765f8a65e52b) # Conflicts: # erpnext/buying/doctype/buying_settings/buying_settings.json * refactor: rewrite `blanket_order.py` queries in `QB` (cherry picked from commit f3993783a3fc431a2909b445e9d09d9f584ff73e) * fix: don't map item row having `0` qty (cherry picked from commit fc1088d9c4787b12bd9734597604492044eff4a0) * feat: consider `over_order_allowance` while validating order qty (cherry picked from commit 8bcbc45add7767ac947fa7c9b3aaca99fc9dda9b) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.py * feat: add field `Over Order Allowance (%)` in `Selling Settings` (cherry picked from commit d7da8928ac44df3a84f6099fc7bfbc9a9161be20) # Conflicts: # erpnext/selling/doctype/selling_settings/selling_settings.json * feat: consider `over_order_allowance` while validating sales order qty (cherry picked from commit 53701c37b18c7aecfaa00efabf4d3be768e59cb3) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.py * test: add test cases for `Over Order Allowance` against `Blanket Order` (cherry picked from commit 66f650061dbae8c1093878f5b808e2a62f3a144a) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../buying_settings/buying_settings.json | 112 +++++----- .../doctype/purchase_order/purchase_order.py | 4 + .../doctype/blanket_order/blanket_order.js | 6 + .../doctype/blanket_order/blanket_order.py | 67 ++++-- .../blanket_order/test_blanket_order.py | 27 +++ .../doctype/sales_order/sales_order.py | 4 + .../selling_settings/selling_settings.json | 192 +++++++++--------- 7 files changed, 254 insertions(+), 158 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index b828a43d3cf..52465c1a962 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -14,6 +14,7 @@ "column_break_3", "po_required", "pr_required", + "over_order_allowance", "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", @@ -42,57 +43,6 @@ "label": "Default Buying Price List", "options": "Price List" }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "po_required", - "fieldtype": "Select", - "label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?", - "options": "No\nYes" - }, - { - "fieldname": "pr_required", - "fieldtype": "Select", - "label": "Is Purchase Receipt Required for Purchase Invoice Creation?", - "options": "No\nYes" - }, - { - "default": "0", - "fieldname": "maintain_same_rate", - "fieldtype": "Check", - "label": "Maintain Same Rate Throughout the Purchase Cycle" - }, - { - "default": "0", - "fieldname": "allow_multiple_items", - "fieldtype": "Check", - "label": "Allow Item To Be Added Multiple Times in a Transaction" - }, - { - "fieldname": "subcontract", - "fieldtype": "Section Break", - "label": "Subcontract" - }, - { - "default": "Material Transferred for Subcontract", - "fieldname": "backflush_raw_materials_of_subcontract_based_on", - "fieldtype": "Select", - "label": "Backflush Raw Materials of Subcontract Based On", - "options": "BOM\nMaterial Transferred for Subcontract" - }, - { - "depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"BOM\"", - "description": "Percentage you are allowed to transfer more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to transfer 110 units.", - "fieldname": "over_transfer_allowance", - "fieldtype": "Float", - "label": "Over Transfer Allowance (%)" - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, { "default": "Stop", "depends_on": "maintain_same_rate", @@ -110,12 +60,70 @@ "label": "Role Allowed to Override Stop Action", "options": "Role" }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "po_required", + "fieldtype": "Select", + "label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?", + "options": "No\nYes" + }, + { + "fieldname": "pr_required", + "fieldtype": "Select", + "label": "Is Purchase Receipt Required for Purchase Invoice Creation?", + "options": "No\nYes" + }, + { + "default": "0", + "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", + "fieldname": "over_order_allowance", + "fieldtype": "Float", + "label": "Over Order Allowance (%)" + }, + { + "default": "0", + "fieldname": "maintain_same_rate", + "fieldtype": "Check", + "label": "Maintain Same Rate Throughout the Purchase Cycle" + }, + { + "default": "0", + "fieldname": "allow_multiple_items", + "fieldtype": "Check", + "label": "Allow Item To Be Added Multiple Times in a Transaction" + }, { "default": "1", "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.", "fieldname": "bill_for_rejected_quantity_in_purchase_invoice", "fieldtype": "Check", "label": "Bill for Rejected Quantity in Purchase Invoice" + }, + { + "fieldname": "subcontract", + "fieldtype": "Section Break", + "label": "Subcontract" + }, + { + "default": "Material Transferred for Subcontract", + "fieldname": "backflush_raw_materials_of_subcontract_based_on", + "fieldtype": "Select", + "label": "Backflush Raw Materials of Subcontract Based On", + "options": "BOM\nMaterial Transferred for Subcontract" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"BOM\"", + "description": "Percentage you are allowed to transfer more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to transfer 110 units.", + "fieldname": "over_transfer_allowance", + "fieldtype": "Float", + "label": "Over Transfer Allowance (%)" } ], "icon": "fa fa-cog", @@ -123,7 +131,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-08 19:26:23.548837", + "modified": "2023-03-22 13:01:49.640869", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 3888622d563..9a2495ed484 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category from erpnext.accounts.party import get_party_account, get_party_account_currency from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController +from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( + validate_against_blanket_order, +) from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty @@ -72,6 +75,7 @@ class PurchaseOrder(BuyingController): self.validate_bom_for_subcontracting_items() self.create_raw_materials_supplied("supplied_items") self.set_received_qty_for_drop_ship_items() + validate_against_blanket_order(self) validate_inter_company_party( self.doctype, self.supplier, self.company, self.inter_company_order_reference ) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index d3bb33e86e0..7b26a14a57b 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js @@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', { }, setup: function(frm) { + frm.custom_make_buttons = { + 'Purchase Order': 'Purchase Order', + 'Sales Order': 'Sales Order', + 'Quotation': 'Quotation', + }; + frm.add_fetch("customer", "customer_name", "customer_name"); frm.add_fetch("supplier", "supplier_name", "supplier_name"); }, diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index ff2140199de..32f1c365ade 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import flt, getdate from erpnext.stock.doctype.item.item import get_item_defaults @@ -29,21 +30,23 @@ class BlanketOrder(Document): def update_ordered_qty(self): ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order" + + trans = frappe.qb.DocType(ref_doctype) + trans_item = frappe.qb.DocType(f"{ref_doctype} Item") + item_ordered_qty = frappe._dict( - frappe.db.sql( - """ - select trans_item.item_code, sum(trans_item.stock_qty) as qty - from `tab{0} Item` trans_item, `tab{0}` trans - where trans.name = trans_item.parent - and trans_item.blanket_order=%s - and trans.docstatus=1 - and trans.status not in ('Closed', 'Stopped') - group by trans_item.item_code - """.format( - ref_doctype - ), - self.name, - ) + ( + frappe.qb.from_(trans_item) + .from_(trans) + .select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty")) + .where( + (trans.name == trans_item.parent) + & (trans_item.blanket_order == self.name) + & (trans.docstatus == 1) + & (trans.status.notin(["Stopped", "Closed"])) + ) + .groupby(trans_item.item_code) + ).run() ) for d in self.items: @@ -79,7 +82,43 @@ def make_order(source_name): "doctype": doctype + " Item", "field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"}, "postprocess": update_item, + "condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0, }, }, ) return target_doc + + +def validate_against_blanket_order(order_doc): + if order_doc.doctype in ("Sales Order", "Purchase Order"): + order_data = {} + + for item in order_doc.get("items"): + if item.against_blanket_order and item.blanket_order: + if item.blanket_order in order_data: + if item.item_code in order_data[item.blanket_order]: + order_data[item.blanket_order][item.item_code] += item.qty + else: + order_data[item.blanket_order][item.item_code] = item.qty + else: + order_data[item.blanket_order] = {item.item_code: item.qty} + + if order_data: + allowance = flt( + frappe.db.get_single_value( + "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", + "over_order_allowance", + ) + ) + for bo_name, item_data in order_data.items(): + bo_doc = frappe.get_doc("Blanket Order", bo_name) + for item in bo_doc.get("items"): + if item.item_code in item_data: + remaining_qty = item.qty - item.ordered_qty + allowed_qty = remaining_qty + (remaining_qty * (allowance / 100)) + if allowed_qty < item_data[item.item_code]: + frappe.throw( + _("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format( + item.item_code, allowed_qty, bo_name + ) + ) diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 2f1f3ae0f52..58f3c950598 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase): po1.currency = get_company_currency(po1.company) self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) + def test_over_order_allowance(self): + # Sales Order + bo = make_blanket_order(blanket_order_type="Selling", quantity=100) + + frappe.flags.args.doctype = "Sales Order" + so = make_order(bo.name) + so.currency = get_company_currency(so.company) + so.delivery_date = today() + so.items[0].qty = 110 + self.assertRaises(frappe.ValidationError, so.submit) + + frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) + so.submit() + + # Purchase Order + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100) + + frappe.flags.args.doctype = "Purchase Order" + po = make_order(bo.name) + po.currency = get_company_currency(po.company) + po.schedule_date = today() + po.items[0].qty = 110 + self.assertRaises(frappe.ValidationError, po.submit) + + frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) + po.submit() + def make_blanket_order(**args): args = frappe._dict(args) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 865b9585618..2f2a06f6fb1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -22,6 +22,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_party_account from erpnext.controllers.selling_controller import SellingController +from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( + validate_against_blanket_order, +) from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, ) @@ -53,6 +56,7 @@ class SalesOrder(SellingController): self.validate_warehouse() self.validate_drop_ship() self.validate_serial_no_based_delivery() + validate_against_blanket_order(self) validate_inter_company_party( self.doctype, self.customer, self.company, self.inter_company_order_reference ) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index ce976547dcd..a51993ff7ff 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -30,12 +30,18 @@ "so_required", "dn_required", "sales_update_frequency", + "over_order_allowance", "allow_multiple_items", "allow_against_multiple_purchase_orders", "hide_tax_id", "allow_sales_order_creation_for_expired_quotation" ], "fields": [ + { + "fieldname": "customer_defaults_section", + "fieldtype": "Section Break", + "label": "Customer Defaults" + }, { "default": "Customer Name", "fieldname": "cust_master_name", @@ -44,13 +50,6 @@ "label": "Customer Naming By", "options": "Customer Name\nNaming Series\nAuto Name" }, - { - "fieldname": "campaign_naming_by", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Campaign Naming By", - "options": "Campaign Name\nNaming Series\nAuto Name" - }, { "fieldname": "customer_group", "fieldtype": "Link", @@ -58,6 +57,10 @@ "label": "Default Customer Group", "options": "Customer Group" }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { "fieldname": "territory", "fieldtype": "Link", @@ -66,11 +69,31 @@ "options": "Territory" }, { - "fieldname": "selling_price_list", - "fieldtype": "Link", + "fieldname": "crm_settings_section", + "fieldtype": "Section Break", + "label": "CRM Settings" + }, + { + "fieldname": "campaign_naming_by", + "fieldtype": "Select", "in_list_view": 1, - "label": "Default Price List", - "options": "Price List" + "label": "Campaign Naming By", + "options": "Campaign Name\nNaming Series\nAuto Name" + }, + { + "fieldname": "contract_naming_by", + "fieldtype": "Select", + "label": "Contract Naming By", + "options": "Party Name\nNaming Series" + }, + { + "fieldname": "default_valid_till", + "fieldtype": "Data", + "label": "Default Quotation Validity Days" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" }, { "default": "15", @@ -80,9 +103,65 @@ "label": "Close Opportunity After Days" }, { - "fieldname": "default_valid_till", - "fieldtype": "Data", - "label": "Default Quotation Validity Days" + "fieldname": "item_price_settings_section", + "fieldtype": "Section Break", + "label": "Item Price Settings" + }, + { + "fieldname": "selling_price_list", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Price List", + "options": "Price List" + }, + { + "default": "Stop", + "depends_on": "maintain_same_sales_rate", + "fieldname": "maintain_same_rate_action", + "fieldtype": "Select", + "label": "Action if Same Rate is Not Maintained Throughout Sales Cycle", + "mandatory_depends_on": "maintain_same_sales_rate", + "options": "Stop\nWarn" + }, + { + "depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'", + "fieldname": "role_to_override_stop_action", + "fieldtype": "Link", + "label": "Role Allowed to Override Stop Action", + "options": "Role" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "maintain_same_sales_rate", + "fieldtype": "Check", + "label": "Maintain Same Rate Throughout Sales Cycle" + }, + { + "default": "0", + "fieldname": "editable_price_list_rate", + "fieldtype": "Check", + "label": "Allow User to Edit Price List Rate in Transactions" + }, + { + "default": "0", + "fieldname": "validate_selling_price", + "fieldtype": "Check", + "label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate" + }, + { + "default": "0", + "fieldname": "editable_bundle_item_rates", + "fieldtype": "Check", + "label": "Calculate Product Bundle Price based on Child Items' Rates" + }, + { + "fieldname": "sales_transactions_settings_section", + "fieldtype": "Section Break", + "label": "Transaction Settings" }, { "fieldname": "so_required", @@ -107,15 +186,10 @@ }, { "default": "0", - "fieldname": "maintain_same_sales_rate", - "fieldtype": "Check", - "label": "Maintain Same Rate Throughout Sales Cycle" - }, - { - "default": "0", - "fieldname": "editable_price_list_rate", - "fieldtype": "Check", - "label": "Allow User to Edit Price List Rate in Transactions" + "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", + "fieldname": "over_order_allowance", + "fieldtype": "Float", + "label": "Over Order Allowance (%)" }, { "default": "0", @@ -129,83 +203,17 @@ "fieldtype": "Check", "label": "Allow Multiple Sales Orders Against a Customer's Purchase Order" }, - { - "default": "0", - "fieldname": "validate_selling_price", - "fieldtype": "Check", - "label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate" - }, { "default": "0", "fieldname": "hide_tax_id", "fieldtype": "Check", "label": "Hide Customer's Tax ID from Sales Transactions" }, - { - "default": "Stop", - "depends_on": "maintain_same_sales_rate", - "fieldname": "maintain_same_rate_action", - "fieldtype": "Select", - "label": "Action if Same Rate is Not Maintained Throughout Sales Cycle", - "mandatory_depends_on": "maintain_same_sales_rate", - "options": "Stop\nWarn" - }, - { - "depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'", - "fieldname": "role_to_override_stop_action", - "fieldtype": "Link", - "label": "Role Allowed to Override Stop Action", - "options": "Role" - }, - { - "fieldname": "column_break_15", - "fieldtype": "Column Break" - }, { "default": "0", - "fieldname": "editable_bundle_item_rates", + "fieldname": "allow_sales_order_creation_for_expired_quotation", "fieldtype": "Check", - "label": "Calculate Product Bundle Price based on Child Items' Rates" - }, - { - "fieldname": "customer_defaults_section", - "fieldtype": "Section Break", - "label": "Customer Defaults" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "crm_settings_section", - "fieldtype": "Section Break", - "label": "CRM Settings" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "fieldname": "item_price_settings_section", - "fieldtype": "Section Break", - "label": "Item Price Settings" - }, - { - "fieldname": "sales_transactions_settings_section", - "fieldtype": "Section Break", - "label": "Transaction Settings" - }, - { - "fieldname": "contract_naming_by", - "fieldtype": "Select", - "label": "Contract Naming By", - "options": "Party Name\nNaming Series" - }, - { - "default": "0", - "fieldname": "allow_sales_order_creation_for_expired_quotation", - "fieldtype": "Check", - "label": "Allow Sales Order Creation For Expired Quotation" + "label": "Allow Sales Order Creation For Expired Quotation" } ], "icon": "fa fa-cog", @@ -213,7 +221,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-04 12:37:53.380857", + "modified": "2023-03-22 13:09:38.513317", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From 3574d490dbf2b3e71722e60a73a50d8af371a2d1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 22 Mar 2023 16:52:07 +0530 Subject: [PATCH 11/18] fix: valuation rate issue while making stock entry from PO --- erpnext/buying/doctype/purchase_order/purchase_order.py | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 9a2495ed484..a28b65bf907 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -644,7 +644,7 @@ def make_rm_stock_entry(purchase_order, rm_items): } stock_entry.add_to_stock_entry_detail(items_dict) - stock_entry.set_missing_values() + stock_entry.set_missing_values(raise_error_if_no_rate=False) return stock_entry.as_dict() else: frappe.throw(_("No Items selected for transfer")) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4711d039c36..4d96e544676 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2229,11 +2229,11 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) - def set_missing_values(self): + def set_missing_values(self, raise_error_if_no_rate=True): "Updates rate and availability of all the items of mapped doc." self.set_transfer_qty() self.set_actual_qty() - self.calculate_rate_and_amount() + self.calculate_rate_and_amount(raise_error_if_no_rate=raise_error_if_no_rate) @frappe.whitelist() From 7f83d15bda8c8df80ff3f3648af46c6e45e48b01 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 23 Mar 2023 12:58:54 +0530 Subject: [PATCH 12/18] fix: unset address and contact on trash (backport #34495) (#34561) fix: unset address and contact on trash (#34495) * fix(Customer): unset address and contact on trash * fix(Supplier): unset address and contact on trash --------- Co-authored-by: Sagar Sharma (cherry picked from commit f7bf1b8a0c3327a75d24b35db2abb438edf4a2c4) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/buying/doctype/supplier/supplier.py | 15 +++------------ erpnext/selling/doctype/customer/customer.py | 15 +++------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 6fdeaaa4c1e..3cfb796c591 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -128,18 +128,9 @@ class Supplier(TransactionBase): def on_trash(self): if self.supplier_primary_contact: - frappe.db.sql( - """ - UPDATE `tabSupplier` - SET - supplier_primary_contact=null, - supplier_primary_address=null, - mobile_no=null, - email_id=null, - primary_address=null - WHERE name=%(name)s""", - {"name": self.name}, - ) + self.db_set("supplier_primary_contact", None) + if self.supplier_primary_address: + self.db_set("supplier_primary_address", None) delete_contact_and_address("Supplier", self.name) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 35e0b0de407..483c79bbdbb 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -275,18 +275,9 @@ class Customer(TransactionBase): def on_trash(self): if self.customer_primary_contact: - frappe.db.sql( - """ - UPDATE `tabCustomer` - SET - customer_primary_contact=null, - customer_primary_address=null, - mobile_no=null, - email_id=null, - primary_address=null - WHERE name=%(name)s""", - {"name": self.name}, - ) + self.db_set("customer_primary_contact", None) + if self.customer_primary_address: + self.db_set("customer_primary_address", None) delete_contact_and_address("Customer", self.name) if self.lead_name: From c8bde399e5a18aec34daa002cc708087adcd8325 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 23 Mar 2023 19:44:13 +0530 Subject: [PATCH 13/18] fix: recalculate WDV rate after asset repair [v13] (#34567) fix: recalculate wdv rate after asset repair --- erpnext/assets/doctype/asset/asset.py | 30 ++++++++++--------- .../doctype/asset_repair/asset_repair.py | 24 --------------- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e46cdb9fc64..662411e0510 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -380,19 +380,12 @@ class Asset(AccountsController): value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount")) # Adjust depreciation amount in the last period based on the expected value after useful life - if ( - finance_book.expected_value_after_useful_life - and ( - ( - n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != finance_book.expected_value_after_useful_life - ) - or value_after_depreciation < finance_book.expected_value_after_useful_life - ) - and ( - not self.flags.increase_in_asset_value_due_to_repair - or not finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance") + if finance_book.expected_value_after_useful_life and ( + ( + n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life ) + or value_after_depreciation < finance_book.expected_value_after_useful_life ): depreciation_amount += ( value_after_depreciation - finance_book.expected_value_after_useful_life @@ -903,10 +896,19 @@ class Asset(AccountsController): return 200.0 / args.get("total_number_of_depreciations") if args.get("depreciation_method") == "Written Down Value": - if args.get("rate_of_depreciation") and on_validate: + if ( + args.get("rate_of_depreciation") + and on_validate + and not self.flags.increase_in_asset_value_due_to_repair + ): return args.get("rate_of_depreciation") - value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) + if self.flags.increase_in_asset_value_due_to_repair: + value = flt(args.get("expected_value_after_useful_life")) / flt( + args.get("value_after_depreciation") + ) + else: + value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2)) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index edcbf3e2fd0..19df1b641d3 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -58,8 +58,6 @@ class AssetRepair(AccountsController): self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.prepare_depreciation_data() - if self.asset_doc.calculate_depreciation: - self.update_asset_expected_value_after_useful_life() self.asset_doc.save() def before_cancel(self): @@ -84,8 +82,6 @@ class AssetRepair(AccountsController): self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.prepare_depreciation_data() - if self.asset_doc.calculate_depreciation: - self.update_asset_expected_value_after_useful_life() self.asset_doc.save() def after_delete(self): @@ -106,26 +102,6 @@ class AssetRepair(AccountsController): title=_("Missing Warehouse"), ) - def update_asset_expected_value_after_useful_life(self): - for row in self.asset_doc.get("finance_books"): - if row.depreciation_method in ("Written Down Value", "Double Declining Balance"): - accumulated_depreciation_after_full_schedule = [ - d.accumulated_depreciation_amount - for d in self.asset_doc.get("schedules") - if cint(d.finance_book_id) == row.idx - ] - - accumulated_depreciation_after_full_schedule = max( - accumulated_depreciation_after_full_schedule - ) - - asset_value_after_full_schedule = flt( - flt(row.value_after_depreciation) - flt(accumulated_depreciation_after_full_schedule), - row.precision("expected_value_after_useful_life"), - ) - - row.expected_value_after_useful_life = asset_value_after_full_schedule - def increase_asset_value(self): total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() From dbe289e734fd13709bd6b1c8963d604b070e116c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:06:27 +0530 Subject: [PATCH 14/18] fix: Search field not working for customer, supplier (#32693) * fix: searchfield not working for cuctsomer, supplier as per customize form (cherry picked from commit 46d148defd59cbb1b9147e035aa6de41ee5fd099) * test: added test case to validate seachfields for customer, supplier (cherry picked from commit 5f84993bae5df78e257cc2bfc41c123a1122a0b6) * fix: not able to select customer / supplier --------- Co-authored-by: Rohit Waghchaure --- .../buying/doctype/supplier/test_supplier.py | 39 +++++++++++++++++++ erpnext/controllers/queries.py | 21 +++++----- .../selling/doctype/customer/test_customer.py | 32 +++++++++++++++ 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index b3cb0e82bdd..926083a2d75 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase @@ -151,6 +152,44 @@ class TestSupplier(FrappeTestCase): # Rollback address.delete() + def test_serach_fields_for_supplier(self): + from erpnext.controllers.queries import supplier_query + + frappe.db.set_value("Buying Settings", None, "supp_master_name", "Naming Series") + + supplier_name = create_supplier(supplier_name="Test Supplier 1").name + + make_property_setter( + "Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype" + ) + + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertTrue("supplier_type" not in data[0]) + + make_property_setter( + "Supplier", + None, + "search_fields", + "supplier_group, supplier_type", + "Data", + for_doctype="Doctype", + ) + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertEqual(data[0].supplier_type, "Company") + self.assertTrue("supplier_type" in data[0]) + + frappe.db.set_value("Buying Settings", None, "supp_master_name", "Supplier Name") + def create_supplier(**args): args = frappe._dict(args) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 53a17422004..e39ef9d8e40 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -78,18 +78,16 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def customer_query(doctype, txt, searchfield, start, page_len, filters): +def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): doctype = "Customer" conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") - if cust_master_name == "Customer Name": - fields = ["name", "customer_group", "territory"] - else: - fields = ["name", "customer_name", "customer_group", "territory"] + fields = ["name"] + if cust_master_name != "Customer Name": + fields.append("customer_name") fields = get_fields(doctype, fields) - searchfields = frappe.get_meta(doctype).get_search_fields() searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) @@ -112,20 +110,20 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): } ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) # searches for supplier @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def supplier_query(doctype, txt, searchfield, start, page_len, filters): +def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): doctype = "Supplier" supp_master_name = frappe.defaults.get_user_default("supp_master_name") - if supp_master_name == "Supplier Name": - fields = ["name", "supplier_group"] - else: - fields = ["name", "supplier_name", "supplier_group"] + fields = ["name"] + if supp_master_name != "Supplier Name": + fields.append("supplier_name") fields = get_fields(doctype, fields) @@ -145,6 +143,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 5c65cb68ef5..1bc3b72d9b7 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase from frappe.utils import flt @@ -343,6 +344,37 @@ class TestCustomer(FrappeTestCase): due_date = get_due_date("2017-01-22", "Customer", "_Test Customer") self.assertEqual(due_date, "2017-01-22") + def test_serach_fields_for_customer(self): + from erpnext.controllers.queries import customer_query + + frappe.db.set_value("Selling Settings", None, "cust_master_name", "Naming Series") + + make_property_setter( + "Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype" + ) + + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertTrue("territory" not in data[0]) + + make_property_setter( + "Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype" + ) + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertEqual(data[0].territory, "_Test Territory") + self.assertTrue("territory" in data[0]) + + frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name") + def get_customer_dict(customer_name): return { From 4bdea436e35ee4c2ea7f8a7aae33e3ca49d4e303 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 16:49:20 +0530 Subject: [PATCH 15/18] fix: Party Name in SOA print when viewed from Customer/Supplier master (#34597) fix: Party Name in SOA print when viewed from Customer/Supplier master (#34597) fix: Party Name in SOA print when viewd from Customer/Supplier master (cherry picked from commit 50c1172f29ac695ebb9b86f4947fb580380b58a6) Co-authored-by: Deepesh Garg --- erpnext/accounts/report/general_ledger/general_ledger.js | 5 ++--- erpnext/buying/doctype/supplier/supplier.js | 2 +- erpnext/selling/doctype/customer/customer.js | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 010284c2ea5..2100f26c1ec 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = { { "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "default": "", + "fieldtype": "Autocomplete", + options: Object.keys(frappe.boot.party_account_types), on_change: function() { frappe.query_report.set_filter_value('party', ""); } diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index f0899b06b57..1ae6f036474 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", { // custom buttons frm.add_custom_button(__('Accounting Ledger'), function () { frappe.set_route('query-report', 'General Ledger', - { party_type: 'Supplier', party: frm.doc.name }); + { party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name }); }, __("View")); frm.add_custom_button(__('Accounts Payable'), function () { diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 2b8d45b070f..5a75cb222ed 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -123,7 +123,7 @@ frappe.ui.form.on("Customer", { frm.add_custom_button(__('Accounting Ledger'), function () { frappe.set_route('query-report', 'General Ledger', - {party_type: 'Customer', party: frm.doc.name}); + {party_type: 'Customer', party: frm.doc.name, party_name: frm.doc.customer_name}); }, __('View')); frm.add_custom_button(__('Pricing Rule'), function () { From 2e9e6eef05bee57d144eca5a6ccafca427fab1e3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Mar 2023 17:25:33 +0530 Subject: [PATCH 16/18] chore: Update ubuntu version (#34620) --- .github/workflows/patch.yml | 2 +- .github/workflows/server-tests.yml | 2 +- .github/workflows/translation_linter.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 30ca22aedc5..2f7398e019e 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -14,7 +14,7 @@ concurrency: jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 timeout-minutes: 60 name: Patch Test diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 5bf42fb7ffe..d5c1e30f1eb 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -18,7 +18,7 @@ concurrency: jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 timeout-minutes: 60 strategy: diff --git a/.github/workflows/translation_linter.yml b/.github/workflows/translation_linter.yml index 4becaebd6b4..af7e0e77528 100644 --- a/.github/workflows/translation_linter.yml +++ b/.github/workflows/translation_linter.yml @@ -8,7 +8,7 @@ on: jobs: check_translation: name: Translation Syntax Check - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Setup python3 From 3aab6e6fa8c241770c78763d0b0db1ed4e1c9f33 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 18:38:52 +0530 Subject: [PATCH 17/18] fix: Percentage billing in Sales Order (#34606) * fix: Percentage billing in Sales Order (#34606) (cherry picked from commit 12ad2aa2e5f5f173c9f52c07fb95e00b069dd403) # Conflicts: # erpnext/controllers/status_updater.py * chore: resolve conflicts --------- Co-authored-by: Deepesh Garg --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 23dad95fa0d..30874f8160d 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -450,7 +450,7 @@ class StatusUpdater(Document): ifnull((select ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0) / sum(abs(%(target_ref_field)s)) * 100 - from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6) + from `tab%(target_dt)s` where parent='%(name)s' and parenttype='%(target_parent_dt)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6) %(update_modified)s where name='%(name)s'""" % args From e53a96ae1d87229fcd6fcaf5ccfb6040e1a14095 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 21:09:09 +0530 Subject: [PATCH 18/18] fix: incorrect `Opening Value` in `Stock Balance` report (backport #34461) (#34622) * fix: incorrect `Opening Value` in `Stock Balance` report (cherry picked from commit b04a101c11d8c3757303c21dd1496256ca01a240) # Conflicts: # erpnext/stock/report/stock_balance/stock_balance.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../report/stock_balance/stock_balance.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 261383d4a20..3e1471fbb7a 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -6,6 +6,7 @@ from operator import itemgetter import frappe from frappe import _ +from frappe.query_builder.functions import Coalesce from frappe.utils import cint, date_diff, flt, getdate from six import iteritems @@ -276,11 +277,39 @@ def get_stock_ledger_entries(filters, items): ) +def get_opening_vouchers(to_date): + opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []} + + se = frappe.qb.DocType("Stock Entry") + sr = frappe.qb.DocType("Stock Reconciliation") + + vouchers_data = ( + frappe.qb.from_( + ( + frappe.qb.from_(se) + .select(se.name, Coalesce("Stock Entry").as_("voucher_type")) + .where((se.docstatus == 1) & (se.posting_date <= to_date) & (se.is_opening == "Yes")) + ) + + ( + frappe.qb.from_(sr) + .select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type")) + .where((sr.docstatus == 1) & (sr.posting_date <= to_date) & (sr.purpose == "Opening Stock")) + ) + ).select("voucher_type", "name") + ).run(as_dict=True) + + if vouchers_data: + for d in vouchers_data: + opening_vouchers[d.voucher_type].append(d.name) + + return opening_vouchers + + def get_item_warehouse_map(filters, sle): iwb_map = {} from_date = getdate(filters.get("from_date")) to_date = getdate(filters.get("to_date")) - + opening_vouchers = get_opening_vouchers(to_date) float_precision = cint(frappe.db.get_default("float_precision")) or 3 for d in sle: @@ -309,11 +338,7 @@ def get_item_warehouse_map(filters, sle): value_diff = flt(d.stock_value_difference) - if d.posting_date < from_date or ( - d.posting_date == from_date - and d.voucher_type == "Stock Reconciliation" - and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock" - ): + if d.posting_date < from_date or d.voucher_no in opening_vouchers.get(d.voucher_type, []): qty_dict.opening_qty += qty_diff qty_dict.opening_val += value_diff