From 98198d35eaa8909396a6ced5ad274931b7f90dcf Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 15 Dec 2023 12:30:15 +0530 Subject: [PATCH 01/24] fix: skip jvs against bank accounts (cherry picked from commit f7b2380ec1cbe5e58755f88ca08cb052b92e05c7) --- .../tds_payable_monthly/tds_payable_monthly.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index f6c7bd3db70..e4953bb1815 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -345,21 +345,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts): if filters.get("party"): party = [filters.get("party")] - query = query.where( - ((gle.account.isin(tds_accounts) & gle.against.isin(party))) - | ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))) - | gle.party.isin(party) + jv_condition = gle.against.isin(party) | ( + (gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")) ) else: party = frappe.get_all(filters.get("party_type"), pluck="name") - query = query.where( - ((gle.account.isin(tds_accounts) & gle.against.isin(party))) - | ( - (gle.voucher_type == "Journal Entry") - & ((gle.party_type == filters.get("party_type")) | (gle.party_type == "")) - ) - | gle.party.isin(party) + jv_condition = gle.against.isin(party) | ( + (gle.voucher_type == "Journal Entry") + & ((gle.party_type == filters.get("party_type")) | (gle.party_type == "")) ) + query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party)) return query From 3c717db3fd2d88f3fbad053c0b71b9a99b62c9b6 Mon Sep 17 00:00:00 2001 From: Richard Case Date: Mon, 30 Oct 2023 00:15:05 +0000 Subject: [PATCH 02/24] feat: in_party_currency option for AR/AP reports --- .../accounts_payable/accounts_payable.js | 7 +++++- .../accounts_receivable.js | 8 +++++-- .../accounts_receivable.py | 22 +++++++++++-------- 3 files changed, 25 insertions(+), 12 deletions(-) mode change 100755 => 100644 erpnext/accounts/report/accounts_receivable/accounts_receivable.py diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 0979cffbf3c..dc2e8a8518b 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -149,11 +149,16 @@ frappe.query_reports["Accounts Payable"] = { "label": __("Revaluation Journals"), "fieldtype": "Check", }, + { + "fieldname": "in_party_currency", + "label": __("In Party Currency"), + "fieldtype": "Check", + }, { "fieldname": "ignore_accounts", "label": __("Group by Voucher"), "fieldtype": "Check", - } + }, ], diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index d6e3098e171..aa492bf51d9 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -181,13 +181,17 @@ frappe.query_reports["Accounts Receivable"] = { "label": __("Revaluation Journals"), "fieldtype": "Check", }, + { + "fieldname": "in_party_currency", + "label": __("In Party Currency"), + "fieldtype": "Check", + }, { "fieldname": "ignore_accounts", - "label": __("Group by Voucher"), + "label": __("Ignore by Voucher"), "fieldtype": "Check", } - ], "formatter": function(value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py old mode 100755 new mode 100644 index f1abc1d4ddb..4d082dc3354 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision # 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters # 7. For overpayment against an invoice with payment terms, there will be an additional row # 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated -# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party -# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" +# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency +# 10. This report is based on Payment Ledger Entries def execute(filters=None): @@ -84,6 +84,9 @@ class ReceivablePayableReport(object): self.total_row_map = {} self.skip_total_row = 1 + if self.filters.get("in_party_currency"): + self.skip_total_row = 1 + def get_data(self): self.get_ple_entries() self.get_sales_invoices_or_customers_based_on_sales_person() @@ -145,7 +148,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.init_subtotal_row(ple.party) - if self.filters.get("group_by_party"): + if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"): self.init_subtotal_row("Total") def get_invoices(self, ple): @@ -224,8 +227,7 @@ class ReceivablePayableReport(object): if not row: return - # amount in "Party Currency", if its supplied. If not, amount in company currency - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency"): amount = ple.amount_in_account_currency else: amount = ple.amount @@ -256,8 +258,10 @@ class ReceivablePayableReport(object): def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) - for field in self.get_currency_fields(): - total_row[field] += row.get(field, 0.0) + if total_row: + for field in self.get_currency_fields(): + total_row[field] += row.get(field, 0.0) + total_row["currency"] = row.get("currency", "") def append_subtotal_row(self, party): sub_total_row = self.total_row_map.get(party) @@ -318,7 +322,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.append_subtotal_row(self.previous_party) if self.data: - self.data.append(self.total_row_map.get("Total")) + self.data.append(self.total_row_map.get("Total", {})) def append_row(self, row): self.allocate_future_payments(row) @@ -449,7 +453,7 @@ class ReceivablePayableReport(object): party_details = self.get_party_details(row.party) or {} row.update(party_details) - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency"): row.currency = row.account_currency else: row.currency = self.company_currency From 08940226950a65cc29161b866ab7920b7e1f6304 Mon Sep 17 00:00:00 2001 From: Richard Case Date: Mon, 30 Oct 2023 00:45:02 +0000 Subject: [PATCH 03/24] chore: update tests --- .../accounts/report/accounts_payable/test_accounts_payable.py | 1 + .../report/accounts_receivable/test_accounts_receivable.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 9f03d92cd50..b4cb25ff1b8 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } data = execute(filters) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index f83285a1a72..dd0842df041 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -581,6 +581,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) From 7eee573fcb37bd0da4cc0b7457e552cb23e8d47f Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sat, 16 Dec 2023 16:29:01 +0000 Subject: [PATCH 04/24] fixup! feat: in_party_currency option for AR/AP reports fix: merge conflict typo --- .../accounts/report/accounts_receivable/accounts_receivable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index aa492bf51d9..28aa7bf9ecf 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -188,7 +188,7 @@ frappe.query_reports["Accounts Receivable"] = { }, { "fieldname": "ignore_accounts", - "label": __("Ignore by Voucher"), + "label": __("Group by Voucher"), "fieldtype": "Check", } From 39aa36e44b66123443c66e3503c1d6bb8c277447 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 16 Dec 2023 21:11:54 +0100 Subject: [PATCH 05/24] fix: groups for current accounts in German CoAs (cherry picked from commit 259f313af75c3cbdcc1af75a8e7d2ad1a3a90c0c) --- .../verified/de_kontenplan_SKR03_gnucash.json | 14 +++++++++++-- ..._kontenplan_SKR04_with_account_number.json | 20 +++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index 741d4283e2f..daf2e21d78c 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -53,8 +53,13 @@ }, "II. Forderungen und sonstige Vermögensgegenstände": { "is_group": 1, - "Ford. a. Lieferungen und Leistungen": { + "Forderungen aus Lieferungen und Leistungen mit Kontokorrent": { "account_number": "1400", + "account_type": "Receivable", + "is_group": 1 + }, + "Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": { + "account_number": "1410", "account_type": "Receivable" }, "Durchlaufende Posten": { @@ -180,8 +185,13 @@ }, "IV. Verbindlichkeiten aus Lieferungen und Leistungen": { "is_group": 1, - "Verbindlichkeiten aus Lieferungen u. Leistungen": { + "Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": { "account_number": "1600", + "account_type": "Payable", + "is_group": 1 + }, + "Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": { + "account_number": "1610", "account_type": "Payable" } }, diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json index 2bf55cfcd04..000ef80ee38 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json @@ -407,13 +407,10 @@ "Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": { "account_number": "9960" }, - "Debitoren": { - "is_group": 1, - "account_number": "10000" - }, - "Forderungen aus Lieferungen und Leistungen": { + "Forderungen aus Lieferungen und Leistungen mit Kontokorrent": { "account_number": "1200", - "account_type": "Receivable" + "account_type": "Receivable", + "is_group": 1 }, "Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": { "account_number": "1210" @@ -1138,18 +1135,15 @@ "Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": { "account_number": "9964" }, - "Kreditoren": { - "account_number": "70000", + "Verb. aus Lieferungen und Leistungen mit Kontokorrent": { + "account_number": "3300", + "account_type": "Payable", "is_group": 1, - "Wareneingangs-­Verrechnungskonto" : { + "Wareneingangs-Verrechnungskonto" : { "account_number": "70001", "account_type": "Stock Received But Not Billed" } }, - "Verb. aus Lieferungen und Leistungen": { - "account_number": "3300", - "account_type": "Payable" - }, "Verb. aus Lieferungen und Leistungen ohne Kontokorrent": { "account_number": "3310" }, From 226d0e0196f8c5defddc600348364d39fa703f27 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 23 Nov 2023 05:46:43 +0000 Subject: [PATCH 06/24] fix: display all item rate stop messages --- erpnext/utilities/transaction_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 7eba35dedd9..b083614a5f7 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -98,6 +98,7 @@ class TransactionBase(StatusUpdater): "Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"] ) + stop_actions = [] for ref_dt, ref_dn_field, ref_link_field in ref_details: reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)] reference_details = self.get_reference_details(reference_names, ref_dt + " Item") @@ -108,7 +109,7 @@ class TransactionBase(StatusUpdater): if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: if action == "Stop": if role_allowed_to_override not in frappe.get_roles(): - frappe.throw( + stop_actions.append( _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate ) @@ -121,6 +122,8 @@ class TransactionBase(StatusUpdater): title=_("Warning"), indicator="orange", ) + if stop_actions: + frappe.throw(stop_actions, as_list=True) def get_reference_details(self, reference_names, reference_doctype): return frappe._dict( From 37a18299d00cade770e79c2702279b0084a58e0a Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:35:35 +0530 Subject: [PATCH 07/24] Revert "fix: unset discount amount based on coupon code (backport #38250)" --- erpnext/public/js/utils/sales_common.js | 431 ------------------------ 1 file changed, 431 deletions(-) delete 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 deleted file mode 100644 index 5514963c966..00000000000 --- a/erpnext/public/js/utils/sales_common.js +++ /dev/null @@ -1,431 +0,0 @@ -// 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 87e8dd0d63068b93c713c4636e97cc3edda411fd Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 19 Dec 2023 15:56:59 +0530 Subject: [PATCH 08/24] fix: unset discount amount from coupon discounts --- erpnext/selling/sales_common.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 80a6b7712fc..3470aee8d8c 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -428,6 +428,11 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran }) } } + + coupon_code() { + this.frm.set_value("discount_amount", 0); + this.frm.set_value("additional_discount_percentage", 0); + } }; frappe.ui.form.on(cur_frm.doctype,"project", function(frm) { From d9e0d55b88e428f80a9e84eb4309f2c153b7028f Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 18 Dec 2023 18:03:53 +0000 Subject: [PATCH 09/24] fix: use party account currency when party account is specified (cherry picked from commit c7b961ffa27c611a1e0a45750b38f1f23b0b0c7f) --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 4a4577720b0..8f7b99aef16 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -227,7 +227,7 @@ class ReceivablePayableReport(object): if not row: return - if self.filters.get("in_party_currency"): + if self.filters.get("in_party_currency") or self.filters.get("party_account"): amount = ple.amount_in_account_currency else: amount = ple.amount @@ -457,7 +457,7 @@ class ReceivablePayableReport(object): party_details = self.get_party_details(row.party) or {} row.update(party_details) - if self.filters.get("in_party_currency"): + if self.filters.get("in_party_currency") or self.filters.get("party_account"): row.currency = row.account_currency else: row.currency = self.company_currency From 30cb2186380f0eafce20fcdbcaf3b209dfd6b46b Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 18 Dec 2023 19:09:25 +0000 Subject: [PATCH 10/24] fix(test): expect account currency when party account is specified. (cherry picked from commit a09241e3c763882a0a0e06b21ccaa0b06f60bc75) --- .../report/accounts_receivable/test_accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index fbfaed6dfda..976935b99f6 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -579,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): filters.update({"party_account": self.debtors_usd}) report = execute(filters)[1] self.assertEqual(len(report), 1) - expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency] + expected_data = [100.0, 100.0, self.debtors_usd, si2.currency] row = report[0] self.assertEqual( expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] From c2f692a4e4f3dd5089fe4949c6cd74574282fdb1 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 20 Dec 2023 17:53:48 +0530 Subject: [PATCH 11/24] fix: allow to set rate manually for service item in BOM (#38880) --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++ erpnext/manufacturing/doctype/bom/test_bom.py | 29 +++++++++++++++++++ .../doctype/bom_item/bom_item.json | 13 +++++++-- erpnext/patches.txt | 1 + .../v14_0/set_maintain_stock_for_bom_item.py | 19 ++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6151af7e3ef..c22af3f22cb 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -661,6 +661,9 @@ class BOM(WebsiteGenerator): base_total_rm_cost = 0 for d in self.get("items"): + if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate": + continue + old_rate = d.rate d.rate = self.get_rm_rate( { diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 051b475bcc3..2debf9191ef 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -698,6 +698,35 @@ class TestBOM(FrappeTestCase): bom.update_cost() self.assertFalse(bom.flags.cost_updated) + def test_bom_with_service_item_cost(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 1000.0}).name + + service_item = make_item(properties={"is_stock_item": 0}).name + + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item, service_item], do_not_save=True) + bom.rm_cost_as_per = "Valuation Rate" + + for row in bom.items: + if row.item_code == service_item: + row.rate = 566.00 + else: + row.rate = 800.00 + + bom.save() + + for row in bom.items: + if row.item_code == service_item: + self.assertEqual(row.is_stock_item, 0) + self.assertEqual(row.rate, 566.00) + else: + self.assertEqual(row.is_stock_item, 1) + def test_do_not_include_manufacturing_and_fixed_items(self): from erpnext.manufacturing.doctype.bom.bom import item_query diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index cb58af1f29a..dfd66120984 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -14,6 +14,7 @@ "bom_no", "source_warehouse", "allow_alternative_item", + "is_stock_item", "section_break_5", "description", "col_break1", @@ -185,7 +186,7 @@ "in_list_view": 1, "label": "Rate", "options": "currency", - "read_only": 1, + "read_only_depends_on": "eval:doc.is_stock_item == 1", "reqd": 1 }, { @@ -284,13 +285,21 @@ "fieldname": "do_not_explode", "fieldtype": "Check", "label": "Do Not Explode" + }, + { + "default": "0", + "fetch_from": "item_code.is_stock_item", + "fieldname": "is_stock_item", + "fieldtype": "Check", + "label": "Is Stock Item", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:35:51.378513", + "modified": "2023-12-20 16:21:55.477883", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d96f45fd676..e0130c57e14 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -355,3 +355,4 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index +erpnext.patches.v14_0.set_maintain_stock_for_bom_item \ No newline at end of file diff --git a/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py new file mode 100644 index 00000000000..f0b618f32d4 --- /dev/null +++ b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + if not frappe.db.exists("BOM", {"docstatus": 1}): + return + + # Added is_stock_item to handle Read Only based on condition for the rate field + frappe.db.sql( + """ + UPDATE + `tabBOM Item` boi, + `tabItem` i + SET + boi.is_stock_item = i.is_stock_item + WHERE + boi.item_code = i.name + """ + ) From 4d1ccd9e27302d161e899fc6b6b02f33a1743844 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:17:41 +0530 Subject: [PATCH 12/24] perf: use estimated rows instead of actual rows (backport #38830) (#38875) * perf: use estimated rows instead of actual rows (#38830) (cherry picked from commit 9983283f95753e7523cf30cc258df0572f88081d) # Conflicts: # erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- .../batch_wise_balance_history.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index b38dba8bb17..31ea576ec1d 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, getdate +from frappe.utils import cint, flt, get_table_name, getdate from pypika import functions as fn from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter @@ -12,11 +12,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter SLE_COUNT_LIMIT = 10_000 +def _estimate_table_row_count(doctype: str): + table = get_table_name(doctype) + return cint( + frappe.db.sql( + f"""select table_rows + from information_schema.tables + where table_name = '{table}' ;""" + )[0][0] + ) + + def execute(filters=None): if not filters: filters = {} - sle_count = frappe.db.count("Stock Ledger Entry") + sle_count = _estimate_table_row_count("Stock Ledger Entry") if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"): frappe.throw(_("Please select either the Item or Warehouse filter to generate the report.")) From 8169c7de0d06ce031f44c30016d1f17744655bf5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:57:37 +0530 Subject: [PATCH 13/24] feat: total_asset_cost field (backport #38879) (#38886) * feat: total_asset_cost field (#38879) (cherry picked from commit d370c60a6c840be23ec4094593b9bbf1d1dca88b) # Conflicts: # erpnext/assets/doctype/asset/asset.json # erpnext/assets/doctype/asset/asset.py # erpnext/patches.txt * chore: resolve conflicts in asset.json * chore: resolve conflicts in asset.py * chore: resolve conflicts in patches.txt --------- Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.json | 13 +++++++++++-- .../assets/doctype/asset_repair/asset_repair.py | 6 ++++++ erpnext/patches.txt | 3 ++- .../v14_0/update_total_asset_cost_field.py | 17 +++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v14_0/update_total_asset_cost_field.py diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 3b0d5d5e7fb..3dc8d29542f 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -35,6 +35,7 @@ "purchase_receipt", "purchase_invoice", "available_for_use_date", + "total_asset_cost", "column_break_23", "gross_purchase_amount", "asset_quantity", @@ -532,6 +533,14 @@ "label": "Capitalized In", "options": "Asset Capitalization", "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus > 0", + "fieldname": "total_asset_cost", + "fieldtype": "Currency", + "label": "Total Asset Cost", + "options": "Company:company:default_currency", + "read_only": 1 } ], "idx": 72, @@ -565,7 +574,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2023-11-20 21:05:45.216899", + "modified": "2023-12-20 16:50:21.128595", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -609,4 +618,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 47453e30308..f978e768af4 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -46,6 +46,9 @@ class AssetRepair(AccountsController): self.increase_asset_value() + if self.capitalize_repair_cost: + self.asset_doc.total_asset_cost += self.repair_cost + if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() @@ -68,6 +71,9 @@ class AssetRepair(AccountsController): self.decrease_asset_value() + if self.capitalize_repair_cost: + self.asset_doc.total_asset_cost -= self.repair_cost + if self.get("stock_consumption"): self.increase_stock_quantity() if self.get("capitalize_repair_cost"): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e0130c57e14..c755a90818d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,7 +352,8 @@ 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.v14_0.update_total_asset_cost_field # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index -erpnext.patches.v14_0.set_maintain_stock_for_bom_item \ No newline at end of file +erpnext.patches.v14_0.set_maintain_stock_for_bom_item diff --git a/erpnext/patches/v14_0/update_total_asset_cost_field.py b/erpnext/patches/v14_0/update_total_asset_cost_field.py new file mode 100644 index 00000000000..57cf71b6134 --- /dev/null +++ b/erpnext/patches/v14_0/update_total_asset_cost_field.py @@ -0,0 +1,17 @@ +import frappe + + +def execute(): + asset = frappe.qb.DocType("Asset") + frappe.qb.update(asset).set(asset.total_asset_cost, asset.gross_purchase_amount).run() + + asset_repair_list = frappe.db.get_all( + "Asset Repair", + filters={"docstatus": 1, "repair_status": "Completed", "capitalize_repair_cost": 1}, + fields=["asset", "repair_cost"], + ) + + for asset_repair in asset_repair_list: + frappe.qb.update(asset).set( + asset.total_asset_cost, asset.total_asset_cost + asset_repair.repair_cost + ).where(asset.name == asset_repair.asset).run() From f2f410093aa688d004637cb3a7a0052498351d8e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 21 Dec 2023 09:31:03 +0530 Subject: [PATCH 14/24] fix: remove rows with zero consumed qty --- erpnext/controllers/subcontracting_controller.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 34d3e700ccc..772fbd54512 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -433,8 +433,11 @@ class SubcontractingController(StockController): self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) self.available_materials[key]["batch_no"][batch_no] = 0 - if abs(qty) > 0 and not new_rm_obj: + if new_rm_obj: + self.remove(rm_obj) + elif abs(qty) > 0: self.__set_consumed_qty(rm_obj, qty) + else: self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) self.__set_serial_nos(item_row, rm_obj) @@ -525,6 +528,10 @@ class SubcontractingController(StockController): (row.item_code, row.get(self.subcontract_data.order_field)) ] -= row.qty + def __reset_idx(self): + for idx, item in enumerate(self.get(self.raw_material_table)): + item.idx = idx + 1 + def __prepare_supplied_items(self): self.initialized_fields() self.__get_subcontract_orders() @@ -532,6 +539,7 @@ class SubcontractingController(StockController): self.get_available_materials() self.__remove_changed_rows() self.__set_supplied_items() + self.__reset_idx() def __validate_batch_no(self, row, key): if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( From 3a668bbe9694fdd6e8265869c6943e42f889ac41 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 21 Dec 2023 14:13:34 +0530 Subject: [PATCH 15/24] =?UTF-8?q?fix:=20reposting=20not=20fixing=20valuati?= =?UTF-8?q?on=20rate=20for=20sales=20return=20using=20movin=E2=80=A6=20(#3?= =?UTF-8?q?8895)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: reposting not fixing valuation rate for sales return using moving average method --- erpnext/controllers/selling_controller.py | 4 +- .../delivery_note/test_delivery_note.py | 53 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 19 ++++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index bac94f98af4..d3157e7037b 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -428,7 +428,9 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty")) - if not d.incoming_rate: + if not d.incoming_rate or ( + get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + ): d.incoming_rate = get_incoming_rate( { "item_code": d.item_code, diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 476b4959813..a931ee2c254 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1382,6 +1382,59 @@ class TestDeliveryNote(FrappeTestCase): self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0) + def test_sales_return_valuation_for_moving_average_case2(self): + # Make DN return + # Make Bakcdated Purchase Receipt and check DN return valuation rate + # The rate should be recalculate based on the backdated purchase receipt + frappe.flags.print_debug_messages = False + item_code = make_item( + "_Test Item Sales Return with MA Case2", + {"is_stock_item": 1, "valuation_method": "Moving Average", "stock_uom": "Nos"}, + ).name + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=100.0, + posting_date=add_days(nowdate(), -5), + ) + + dn = create_delivery_note( + item_code=item_code, + warehouse="_Test Warehouse - _TC", + qty=5, + rate=500, + posting_date=add_days(nowdate(), -4), + ) + + returned_dn = create_delivery_note( + is_return=1, + item_code=item_code, + return_against=dn.name, + qty=-5, + rate=500, + company=dn.company, + warehouse="_Test Warehouse - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + posting_date=add_days(nowdate(), -1), + ) + + self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 100.0) + + # Make backdated purchase receipt + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=200.0, + posting_date=add_days(nowdate(), -3), + ) + + returned_dn.reload() + self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8a262497926..df1f544d7b1 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, + get_incoming_rate, get_or_make_bin, get_valuation_method, ) @@ -701,7 +702,23 @@ class update_entries_after(object): ) if self.valuation_method == "Moving Average": - rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate) + rate = get_incoming_rate( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + "qty": sle.actual_qty, + "serial_no": sle.get("serial_no"), + "batch_no": sle.get("batch_no"), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no, + "allow_zero_valuation": self.allow_zero_rate, + "sle": sle.name, + } + ) + else: rate = get_rate_for_return( sle.voucher_type, From 98bfcc4c758b61f714c53ea0f00246731a30fdaf Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 21 Dec 2023 14:13:55 +0530 Subject: [PATCH 16/24] fix: do not reset the basic rate for the material receipt stock entry (#38896) --- erpnext/stock/doctype/stock_entry/stock_entry.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 70771e77461..0786ce6be2a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -517,7 +517,12 @@ frappe.ui.form.on('Stock Entry', { }, callback: function(r) { if (!r.exc) { - ["actual_qty", "basic_rate"].forEach((field) => { + let fields = ["actual_qty", "basic_rate"]; + if (frm.doc.purpose == "Material Receipt") { + fields = ["actual_qty"]; + } + + fields.forEach((field) => { frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0)); }); frm.events.calculate_basic_amount(frm, child); From 3b9feffc00ec6c1f0e99bd4670030efc71509595 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:56:32 +0530 Subject: [PATCH 17/24] chore: additional_asset_cost field (backport #38904) (#38905) * chore: additional_asset_cost field (#38904) (cherry picked from commit 283763dfb2affa6a0b7bb29e19123c3e1fb27f30) # Conflicts: # erpnext/assets/doctype/asset/asset.py * chore: resolve conflicts in asset.py --------- Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.json | 11 ++++++++++- erpnext/assets/doctype/asset/asset.py | 1 + erpnext/assets/doctype/asset_repair/asset_repair.py | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 3dc8d29542f..91ae62bfad7 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -36,6 +36,7 @@ "purchase_invoice", "available_for_use_date", "total_asset_cost", + "additional_asset_cost", "column_break_23", "gross_purchase_amount", "asset_quantity", @@ -541,6 +542,14 @@ "label": "Total Asset Cost", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus > 0", + "fieldname": "additional_asset_cost", + "fieldtype": "Currency", + "label": "Additional Asset Cost", + "options": "Company:company:default_currency", + "read_only": 1 } ], "idx": 72, @@ -574,7 +583,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2023-12-20 16:50:21.128595", + "modified": "2023-12-21 16:46:20.732869", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 92a349abb3e..f83d7071aa7 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -48,6 +48,7 @@ class Asset(AccountsController): if self.get("schedules"): self.validate_expected_value_after_useful_life() + self.total_asset_cost = self.gross_purchase_amount self.status = self.get_status() def on_submit(self): diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index f978e768af4..b348e13ca83 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -48,6 +48,7 @@ class AssetRepair(AccountsController): if self.capitalize_repair_cost: self.asset_doc.total_asset_cost += self.repair_cost + self.asset_doc.additional_asset_cost += self.repair_cost if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() @@ -73,6 +74,7 @@ class AssetRepair(AccountsController): if self.capitalize_repair_cost: self.asset_doc.total_asset_cost -= self.repair_cost + self.asset_doc.additional_asset_cost -= self.repair_cost if self.get("stock_consumption"): self.increase_stock_quantity() From 2770ca1b659a3217e249b60155434052d0a62730 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 21 Dec 2023 18:49:36 +0530 Subject: [PATCH 18/24] fix: reset the incoming rate on changing of the warehouse (#38909) --- erpnext/selling/sales_common.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 3470aee8d8c..b2a64a5d461 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -200,6 +200,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran item.serial_no = null; } + if (doc.docstatus === 0 && doc.is_return && !doc.return_against) { + item.incoming_rate = 0.0; + } + var has_batch_no; frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => { has_batch_no = r && r.has_batch_no; From b254a72d41bd701cf93752771681b0f55e93a6f7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 21:40:59 +0530 Subject: [PATCH 19/24] perf: Drop unused/duplicate/sub-optimal indexes (backport #38884) (#38912) * perf: Drop unused/duplicate/sub-optimal indexes (#38884) * ci: enable more checks * perf: Drop unused/duplicate indexes (cherry picked from commit 787333896c3710d16b1fa72432db0fe59c74dfa8) # Conflicts: # erpnext/accounts/doctype/gl_entry/gl_entry.json # erpnext/patches.txt * chore: conflicts --------- Co-authored-by: Ankush Menat --- .pre-commit-config.yaml | 6 +++- .../accounts/doctype/gl_entry/gl_entry.json | 8 ++---- .../purchase_invoice/purchase_invoice.py | 4 --- .../doctype/sales_invoice/sales_invoice.py | 4 --- .../purchase_order_item.json | 3 +- erpnext/patches.txt | 2 +- erpnext/stock/doctype/bin/bin.json | 3 +- .../drop_unused_return_against_index.py | 28 +++++++++++++------ erpnext/tests/test_perf.py | 24 ++++++++++++++++ 9 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 erpnext/tests/test_perf.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d70977c07e2..fba2f74a5b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ fail_fast: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.3.0 hooks: - id: trailing-whitespace files: "erpnext.*" @@ -15,6 +15,10 @@ repos: args: ['--branch', 'develop'] - id: check-merge-conflict - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements - repo: https://github.com/PyCQA/flake8 rev: 5.0.4 diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index e6d97a1fb26..592eaecc1c5 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -138,8 +138,7 @@ "label": "Against Voucher Type", "oldfieldname": "against_voucher_type", "oldfieldtype": "Data", - "options": "DocType", - "search_index": 1 + "options": "DocType" }, { "fieldname": "against_voucher", @@ -158,8 +157,7 @@ "label": "Voucher Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "DocType", - "search_index": 1 + "options": "DocType" }, { "fieldname": "voucher_no", @@ -291,4 +289,4 @@ "search_fields": "voucher_no,account,posting_date,against_voucher", "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 944a8fb2364..7171bddac4b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1660,10 +1660,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None): return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) -def on_doctype_update(): - frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"]) - - @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3d18a860361..63a576b5ba8 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2388,10 +2388,6 @@ def get_loyalty_programs(customer): return lp_details -def on_doctype_update(): - frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"]) - - @frappe.whitelist() def create_invoice_discounting(source_name, target_doc=None): invoice = frappe.get_doc("Sales Invoice", source_name) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 45790b80fcb..1a9035c3327 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -123,8 +123,7 @@ "oldfieldname": "item_code", "oldfieldtype": "Link", "options": "Item", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "fieldname": "supplier_part_no", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c755a90818d..e464552fa0c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -355,5 +355,5 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles erpnext.patches.v14_0.update_total_asset_cost_field # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger -erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index +erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 erpnext.patches.v14_0.set_maintain_stock_for_bom_item diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 02684a72419..5284bc410c1 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -51,8 +51,7 @@ "oldfieldtype": "Link", "options": "Item", "read_only": 1, - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "default": "0.00", diff --git a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py index 8fe4ffb58f1..cc29e67fa7b 100644 --- a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py +++ b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py @@ -1,15 +1,27 @@ +import click import frappe +UNUSED_INDEXES = [ + ("Delivery Note", ["customer", "is_return", "return_against"]), + ("Sales Invoice", ["customer", "is_return", "return_against"]), + ("Purchase Invoice", ["supplier", "is_return", "return_against"]), + ("Purchase Receipt", ["supplier", "is_return", "return_against"]), +] + def execute(): - """Drop unused return_against index""" + for doctype, index_fields in UNUSED_INDEXES: + table = f"tab{doctype}" + index_name = frappe.db.get_index_name(index_fields) + drop_index_if_exists(table, index_name) + + +def drop_index_if_exists(table: str, index: str): + if not frappe.db.has_index(table, index): + return try: - frappe.db.sql_ddl( - "ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`" - ) - frappe.db.sql_ddl( - "ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`" - ) + frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + click.echo(f"✓ dropped {index} index from {table}") except Exception: - frappe.log_error("Failed to drop unused index") + frappe.log_error("Failed to drop index") diff --git a/erpnext/tests/test_perf.py b/erpnext/tests/test_perf.py new file mode 100644 index 00000000000..fc17b1dcbda --- /dev/null +++ b/erpnext/tests/test_perf.py @@ -0,0 +1,24 @@ +import frappe +from frappe.tests.utils import FrappeTestCase + +INDEXED_FIELDS = { + "Bin": ["item_code"], + "GL Entry": ["voucher_type", "against_voucher_type"], + "Purchase Order Item": ["item_code"], + "Stock Ledger Entry": ["warehouse"], +} + + +class TestPerformance(FrappeTestCase): + def test_ensure_indexes(self): + # These fields are not explicitly indexed BUT they are prefix in some + # other composite index. If those are removed this test should be + # updated accordingly. + for doctype, fields in INDEXED_FIELDS.items(): + for field in fields: + self.assertTrue( + frappe.db.sql( + f"""SHOW INDEX FROM `tab{doctype}` + WHERE Column_name = "{field}" AND Seq_in_index = 1""" + ) + ) From 28f052d586c2781136b2e6497b7f3731a5b5227e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 24 Dec 2023 16:12:13 +0530 Subject: [PATCH 20/24] fix: first name error on customer creation (#38927) fix: error on customer creation --- erpnext/selling/doctype/customer/customer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 4bbce9b2f00..56388a594a7 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -702,6 +702,7 @@ def make_contact(args, is_primary_contact=1): else: values.update( { + "first_name": args.get("customer_name"), "company_name": args.get("customer_name"), } ) From 81b5e6c5f13da686d4472168b731e7e284a4e9c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 24 Dec 2023 16:24:31 +0530 Subject: [PATCH 21/24] fix: german translations for Lost Quotations (#38435) * fix: german translations for Lost Quotations (#38435) (cherry picked from commit 5952cfa673837e09167591e593e5691f1f121b6e) # Conflicts: # erpnext/translations/de.csv * chore: resolve conflicts --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/translations/de.csv | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 105021cfa75..3a8b7fe1a4a 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -9104,3 +9104,7 @@ Select an item from each set to be used in the Sales Order.,"Wählen Sie aus den Is Alternative,Ist Alternative, Alternative Items,Alternativpositionen, Component Type,Komponententyp, +Lost Quotations,Verlorene Angebote, +Lost Quotations %,Verlorene Angebote %, +Lost Value,Verlorener Wert, +Lost Value %,Verlorener Wert %, From 7577706a9dc0a050cc3ed401f44f15a528b8d573 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Dec 2023 17:22:30 +0530 Subject: [PATCH 22/24] fix: incorrect price list in customer-wise item price report (cherry picked from commit 9a00edb03115b72afb4274e6fcf2dc7d5b431657) --- .../customer_wise_item_price.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py index a58f40362ba..40aa9acc3c6 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py @@ -3,11 +3,11 @@ import frappe -from frappe import _ +from frappe import _, qb +from frappe.query_builder import Criterion from erpnext import get_default_company from erpnext.accounts.party import get_party_details -from erpnext.stock.get_item_details import get_price_list_rate_for def execute(filters=None): @@ -50,6 +50,42 @@ def get_columns(filters=None): ] +def fetch_item_prices( + customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None +): + price_list_map = frappe._dict() + ip = qb.DocType("Item Price") + and_conditions = [] + or_conditions = [] + if items: + and_conditions.append(ip.item_code.isin([x.item_code for x in items])) + and_conditions.append(ip.selling == True) + + or_conditions.append(ip.customer == None) + or_conditions.append(ip.price_list == None) + + if customer: + or_conditions.append(ip.customer == customer) + + if price_list: + or_conditions.append(ip.price_list == price_list) + + if selling_price_list: + or_conditions.append(ip.price_list == selling_price_list) + + res = ( + qb.from_(ip) + .select(ip.item_code, ip.price_list, ip.price_list_rate) + .where(Criterion.all(and_conditions)) + .where(Criterion.any(or_conditions)) + .run(as_dict=True) + ) + for x in res: + price_list_map.update({(x.item_code, x.price_list): x.price_list_rate}) + + return price_list_map + + def get_data(filters=None): data = [] customer_details = get_customer_details(filters) @@ -59,9 +95,17 @@ def get_data(filters=None): "Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code" ) item_stock_map = {item.item_code: item.available for item in item_stock_map} + price_list_map = fetch_item_prices( + customer_details.customer, + customer_details.price_list, + customer_details.selling_price_list, + items, + ) for item in items: - price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0 + price_list_rate = price_list_map.get( + (item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0 + ) available_stock = item_stock_map.get(item.item_code) data.append( From 1eee203f7fb840e3aeb23b5a07231764cb2afef3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:45:54 +0530 Subject: [PATCH 23/24] fix(ux): make PR and PI Item rate field readonly based on `Maintain Same Rate` (backport #38942) (#38943) * fix(ux): make PI Item rate field editable (cherry picked from commit eb5bb9f9a99e54be71c7ae5e24235b5b44692444) * fix(ux): make PI Item rate field readonly based on `Maintain Same Rate` (cherry picked from commit cb9114442b936f03c9c975900282b3b9db62e453) * fix(ux): make PR Item rate field readonly based on `Maintain Same Rate` (cherry picked from commit b1ba2103323c8bfe066c288b4c285cf1f3c5b20b) --------- Co-authored-by: s-aga-r --- .../doctype/purchase_invoice/purchase_invoice.js | 12 ++++++++++++ .../purchase_invoice_item.json | 3 +-- .../doctype/purchase_receipt/purchase_receipt.js | 14 ++++++++++++++ .../purchase_receipt_item.json | 3 +-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index c2aa1d936e5..3b77614607a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -164,6 +164,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } }) }, __("Get Items From")); + + if (!this.frm.doc.is_return) { + frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => { + if (value) { + this.frm.doc.items.forEach((item) => { + this.frm.fields_dict.items.grid.update_docfield_property( + "rate", "read_only", (item.purchase_receipt && item.pr_detail) + ); + }); + } + }); + } } this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); 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 bc56132c60f..3d59d288e4d 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -286,7 +286,6 @@ "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency", - "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)", "reqd": 1 }, { @@ -894,7 +893,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 16:26:05.629780", + "modified": "2023-12-25 22:00:28.043555", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 8966fbcbb3c..314b65ee4f8 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -78,6 +78,20 @@ frappe.ui.form.on("Purchase Receipt", { }, __('Create')); } + if (frm.doc.docstatus === 0) { + if (!frm.doc.is_return) { + frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => { + if (value) { + frm.doc.items.forEach((item) => { + frm.fields_dict.items.grid.update_docfield_property( + "rate", "read_only", (item.purchase_order && item.purchase_order_item) + ); + }); + } + }); + } + } + frm.events.add_custom_buttons(frm); }, 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 6d89e9897af..1a2bb8e4c8e 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -352,7 +352,6 @@ "oldfieldtype": "Currency", "options": "currency", "print_width": "100px", - "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)", "width": "100px" }, { @@ -1055,7 +1054,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 16:12:02.364608", + "modified": "2023-12-25 22:32:09.801965", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From f6f2712db0cd8cec3940e7e31678d909307887b9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 26 Dec 2023 21:59:09 +0530 Subject: [PATCH 24/24] fix: min order qty optional in production plan (backport #38956) (#38957) * fix: min order qty optional in production plan (#38956) * fix: min order qty optional in production plan * fix: test cases (cherry picked from commit b09c9354fb621c4283d6ebde91f3d061ea88f7f6) # Conflicts: # erpnext/manufacturing/doctype/bom/bom.json # erpnext/manufacturing/doctype/production_plan/production_plan.py * chore: fix conflicts * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.json | 5 ++-- .../production_plan/production_plan.js | 2 ++ .../production_plan/production_plan.json | 9 +++++++- .../production_plan/production_plan.py | 18 +++++++++++++-- .../production_plan/test_production_plan.py | 23 +++++++++++++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index d02402299e3..28c33e47fb9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -214,6 +214,7 @@ "options": "\nWork Order\nJob Card" }, { + "default": "1", "fieldname": "conversion_rate", "fieldtype": "Float", "label": "Conversion Rate", @@ -606,7 +607,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:47:58.514795", + "modified": "2023-12-26 19:34:08.159312", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", @@ -645,4 +646,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index dd102b0fae0..cd92263543b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -305,6 +305,8 @@ frappe.ui.form.on('Production Plan', { frappe.throw(__("Select the Warehouse")); } + frm.set_value("consider_minimum_order_qty", 0); + if (frm.doc.ignore_existing_ordered_qty) { frm.events.get_items_for_material_requests(frm); } else { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 49386c4ebc4..257b60c4869 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -48,6 +48,7 @@ "material_request_planning", "include_non_stock_items", "include_subcontracted_items", + "consider_minimum_order_qty", "include_safety_stock", "ignore_existing_ordered_qty", "column_break_25", @@ -423,13 +424,19 @@ "fieldtype": "Link", "label": "Sub Assembly Warehouse", "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "consider_minimum_order_qty", + "fieldtype": "Check", + "label": "Consider Minimum Order Qty" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-03 14:08:11.928027", + "modified": "2023-12-26 16:31:13.740777", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index b546594a4ed..ee2e58bfb46 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1135,7 +1135,14 @@ def get_subitems( def get_material_request_items( - row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict + doc, + row, + sales_order, + company, + ignore_existing_ordered_qty, + include_safety_stock, + warehouse, + bin_dict, ): total_qty = row["qty"] @@ -1144,8 +1151,14 @@ def get_material_request_items( required_qty = total_qty elif total_qty > bin_dict.get("projected_qty", 0): required_qty = total_qty - bin_dict.get("projected_qty", 0) - if required_qty > 0 and required_qty < row["min_order_qty"]: + + if ( + doc.get("consider_minimum_order_qty") + and required_qty > 0 + and required_qty < row["min_order_qty"] + ): required_qty = row["min_order_qty"] + item_group_defaults = get_item_group_defaults(row.item_code, company) if not row["purchase_uom"]: @@ -1483,6 +1496,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d if details.qty > 0: items = get_material_request_items( + doc, details, sales_order, company, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index d546306f080..8c8e6efd92d 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1488,6 +1488,29 @@ class TestProductionPlan(FrappeTestCase): after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) self.assertAlmostEqual(after_qty, before_qty) + def test_min_order_qty_in_pp(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.stock.utils import get_or_make_bin + + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1, "min_order_qty": 1000}).name + + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1) + + pln.for_warehouse = rm_warehouse + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + self.assertEqual(d.get("quantity"), 10.0) + + pln.consider_minimum_order_qty = 1 + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + self.assertEqual(d.get("quantity"), 1000.0) + def create_production_plan(**args): """