From feb452b740f1614cabc36f9f661d10843a992789 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:39:00 +0530 Subject: [PATCH 01/35] fix: supplier removed on selection of item (backport #38712) (#38713) fix: supplier removed on selection of item (#38712) (cherry picked from commit db24e2488247eef326ef52a88dbae8e828893e7e) Co-authored-by: rohitwaghchaure --- erpnext/stock/get_item_details.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d1a9cf26ac..8ccc8c9676 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -357,7 +357,6 @@ def get_basic_details(args, item, overwrite_warehouse=True): "net_amount": 0.0, "discount_percentage": 0.0, "discount_amount": flt(args.discount_amount) or 0.0, - "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), "update_stock": args.get("update_stock") if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"] else 0, @@ -377,6 +376,10 @@ def get_basic_details(args, item, overwrite_warehouse=True): } ) + default_supplier = get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults) + if default_supplier: + out.supplier = default_supplier + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): out.update(calculate_service_end_date(args, item)) From 1dcb065c64ba5c04323f25300b13cbce697d5e63 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:32:48 +0530 Subject: [PATCH 02/35] fix: barcode scanning for the stock entry (backport #38716) (#38718) fix: barcode scanning for the stock entry (#38716) (cherry picked from commit 13cba5068bd1d09bff4204a64a74999876566b6b) Co-authored-by: rohitwaghchaure --- erpnext/public/js/controllers/transaction.js | 1 + erpnext/public/js/utils/barcode_scanner.js | 12 ++++++------ erpnext/public/js/utils/serial_no_batch_selector.js | 5 ++++- erpnext/stock/doctype/stock_entry/stock_entry.js | 8 ++++---- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c1c4b99dca..de65cc5dfe 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -380,6 +380,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } scan_barcode() { + frappe.flags.dialog_set = false; const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); barcode_scanner.process_scan(); } diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index a4f74bdaee..a1ebfe9aa4 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -114,13 +114,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { frappe.run_serially([ () => this.set_selector_trigger_flag(data), + () => this.set_serial_no(row, serial_no), + () => this.set_batch_no(row, batch_no), + () => this.set_barcode(row, barcode), () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => { this.show_scan_message(row.idx, row.item_code, qty); }), () => this.set_barcode_uom(row, uom), - () => this.set_serial_no(row, serial_no), - () => this.set_batch_no(row, batch_no), - () => this.set_barcode(row, barcode), () => this.clean_up(), () => this.revert_selector_flag(), () => resolve(row) @@ -131,10 +131,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // batch and serial selector is reduandant when all info can be added by scan // this flag on item row is used by transaction.js to avoid triggering selector set_selector_trigger_flag(data) { - const {batch_no, serial_no, has_batch_no, has_serial_no} = data; + const {has_batch_no, has_serial_no} = data; - const require_selecting_batch = has_batch_no && !batch_no; - const require_selecting_serial = has_serial_no && !serial_no; + const require_selecting_batch = has_batch_no; + const require_selecting_serial = has_serial_no; if (!(require_selecting_batch || require_selecting_serial)) { frappe.flags.hide_serial_batch_dialog = true; diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 3b9a551b43..7b9cdfef2a 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -31,6 +31,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { secondary_action: () => this.edit_full_form(), }); + this.dialog.show(); + let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty; this.dialog.set_value("qty", qty).then(() => { if (this.item.serial_no) { @@ -40,9 +42,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { this.dialog.set_value("scan_batch_no", this.item.batch_no); frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', ''); } + + this.dialog.fields_dict.entries.grid.refresh(); }); - this.dialog.show(); this.$scan_btn = this.dialog.$wrapper.find(".link-btn"); this.$scan_btn.css("display", "inline"); } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 7334b356ee..7af5d1aa37 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -781,10 +781,9 @@ frappe.ui.form.on('Stock Entry Detail', { }); refresh_field("items"); - let no_batch_serial_number_value = !d.serial_no; - if (d.has_batch_no && !d.has_serial_no) { - // check only batch_no for batched item - no_batch_serial_number_value = !d.batch_no; + let no_batch_serial_number_value = false; + if (d.has_serial_no || d.has_batch_no) { + no_batch_serial_number_value = true; } if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) { @@ -941,6 +940,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle } scan_barcode() { + frappe.flags.dialog_set = false; const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); barcode_scanner.process_scan(); } From f30bede2e03a2662ed17de1664bca3b624057f54 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:00:58 +0100 Subject: [PATCH 03/35] feat: RFQ print preview (cherry picked from commit 27f05145ae700e1177bb2c2541e6d49f73cdbf7e) --- .../request_for_quotation/request_for_quotation.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index eea8cd5cc8..5b8be44296 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -119,6 +119,15 @@ class RequestforQuotation(BuyingController): supplier.quote_status = "Pending" self.send_to_supplier() + def before_print(self, settings=None): + """Use the first suppliers data to render the print preview.""" + if self.vendor or not self.suppliers: + # If a specific supplier is already set, via Tools > Download PDF, + # we don't want to override it. + return + + self.update_supplier_part_no(self.suppliers[0].supplier) + def on_cancel(self): self.db_set("status", "Cancelled") From 3fabca1051567aacc0d404b230fff1b881f249c9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:16:15 +0530 Subject: [PATCH 04/35] Revert "fix(ux): don't update qty blindly" (backport #38728) (#38730) Revert "fix(ux): don't update qty blindly" (#38728) (cherry picked from commit 6851c5042fc5e2d3f45d070af18a33c24fc10794) Co-authored-by: Ankush Menat --- erpnext/public/js/controllers/transaction.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index de65cc5dfe..66216186a1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -472,6 +472,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.pricing_rules = '' return this.frm.call({ method: "erpnext.stock.get_item_details.get_item_details", + child: item, args: { doc: me.frm.doc, args: { @@ -520,19 +521,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe callback: function(r) { if(!r.exc) { frappe.run_serially([ - () => { - var child = locals[cdt][cdn]; - var std_field_list = ["doctype"] - .concat(frappe.model.std_fields_list) - .concat(frappe.model.child_table_field_list); - - for (var key in r.message) { - if (std_field_list.indexOf(key) === -1) { - if (key === "qty" && child[key]) continue; - child[key] = r.message[key]; - } - } - }, () => { var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); From f55b561ff918268c178c7a51822c51cc3c7ed89f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Dec 2023 11:25:21 +0530 Subject: [PATCH 05/35] fix: fetch exc rate of multi currency journals (cherry picked from commit 1b3ba25220ca4ef96df5416331ee2f78376da64a) --- .../payment_reconciliation.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 06ad092595..3ea25d16e6 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -592,6 +592,27 @@ class PaymentReconciliation(Document): invoice_exchange_map.update(purchase_invoice_map) + journals = [ + d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry" + ] + journals.extend( + [d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"] + ) + if journals: + journals = list(set(journals)) + journals_map = frappe._dict( + frappe.db.get_all( + "Journal Entry Account", + filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])}, + fields=[ + "parent as `name`", + "exchange_rate", + ], + as_list=1, + ) + ) + invoice_exchange_map.update(journals_map) + return invoice_exchange_map def validate_allocation(self): From a551660d2a92abb655a573141a378afee5502e29 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:59:21 +0530 Subject: [PATCH 06/35] fix: timezone aware SLA banner (backport #38745) (#38747) fix: timezone aware SLA banner (#38745) (cherry picked from commit eaf86a6461438720fe941100d6feccefbfa3bfed) Co-authored-by: Ankush Menat --- erpnext/public/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index e9d06dfbec..b0ea56833b 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -1077,7 +1077,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { } function get_time_left(timestamp, agreement_status) { - const diff = moment(timestamp).diff(moment()); + const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true)); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed'; let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green'; return {'diff_display': diff_display, 'indicator': indicator}; From f7706211ea9baa79b46584feccec4bef4d5de5be Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:10:51 +0100 Subject: [PATCH 07/35] feat: set lead name from email (cherry picked from commit ceeb724acc54d06ec27f3a3b5e948e5b6236af7c) --- erpnext/crm/doctype/lead/lead.json | 3 ++- erpnext/crm/doctype/lead/lead.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index dafbd9f06d..92f446d57d 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -516,7 +516,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2023-08-28 22:28:00.104413", + "modified": "2023-12-01 18:46:49.468526", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -577,6 +577,7 @@ ], "search_fields": "lead_name,lead_owner,status", "sender_field": "email_id", + "sender_name_field": "lead_name", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 74a172d094..781c4d3168 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -14,6 +14,7 @@ from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_ema from erpnext.accounts.party import set_taxes from erpnext.controllers.selling_controller import SellingController from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events +from erpnext.selling.doctype.customer.customer import parse_full_name class Lead(SellingController, CRMNote): @@ -111,6 +112,10 @@ class Lead(SellingController, CRMNote): return self.contact_doc = self.create_contact() + # leads created by email inbox only have the full name set + if self.lead_name and not any([self.first_name, self.middle_name, self.last_name]): + self.first_name, self.middle_name, self.last_name = parse_full_name(self.lead_name) + def after_insert(self): self.link_to_contact() From ad3a5b58e4ee82f2462756b865183143c133cb21 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 23:02:25 +0530 Subject: [PATCH 08/35] fix: homepage not working (backport #38755) (#38756) fix: homepage not working (#38755) (cherry picked from commit d6201ce5c7b6f981ca6e0adbfa52c1c24ba5de83) Co-authored-by: rohitwaghchaure --- erpnext/portal/doctype/homepage/homepage.js | 8 -------- erpnext/templates/pages/home.html | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js index 6797904424..6739979b98 100644 --- a/erpnext/portal/doctype/homepage/homepage.js +++ b/erpnext/portal/doctype/homepage/homepage.js @@ -2,14 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Homepage', { - setup: function(frm) { - frm.fields_dict["products"].grid.get_field("item").get_query = function() { - return { - filters: {'published': 1} - } - } - }, - refresh: function(frm) { frm.add_custom_button(__('Set Meta Tags'), () => { frappe.utils.set_meta_tag('home'); diff --git a/erpnext/templates/pages/home.html b/erpnext/templates/pages/home.html index 08e0432dcf..b9b435c7c3 100644 --- a/erpnext/templates/pages/home.html +++ b/erpnext/templates/pages/home.html @@ -26,6 +26,26 @@ {{ render_homepage_section(homepage.hero_section_doc) }} {% endif %} + {% if homepage.products %} +
+

{{ _('Products') }}

+ +
+ {% for item in homepage.products %} +
+
+ {{ item.item_name }} +
+
{{ item.item_name }}
+ {{ _('More details') }} +
+
+
+ {% endfor %} +
+
+ {% endif %} + {% if blogs %}

{{ _('Publications') }}

From e7e5727015990aa0d77cf3aa058bdfa2561a7043 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 22 Oct 2023 10:58:39 +0530 Subject: [PATCH 09/35] refactor: exc rate on foreign currency JE from Bank Reconciliation (cherry picked from commit 89f484282a90516c117c31624fb2cc5eab6fb840) --- .../bank_reconciliation_tool.py | 89 ++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 1425e734eb..867dbdcaa1 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -18,6 +18,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s get_entries, ) from erpnext.accounts.utils import get_account_currency, get_balance_on +from erpnext.setup.utils import get_exchange_rate class BankReconciliationTool(Document): @@ -164,29 +165,74 @@ def create_journal_entry_bts( ) company = frappe.get_value("Account", company_account, "company") + company_default_currency = frappe.get_cached_value("Company", company, "default_currency") + company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency") + second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency") + + is_multi_currency = ( + True + if company_default_currency != company_account_currency + or company_default_currency != second_account_currency + else False + ) accounts = [] - # Multi Currency? - accounts.append( - { - "account": second_account, - "credit_in_account_currency": bank_transaction.deposit, - "debit_in_account_currency": bank_transaction.withdrawal, - "party_type": party_type, - "party": party, - "cost_center": get_default_cost_center(company), - } - ) + second_account_dict = { + "account": second_account, + "account_currency": second_account_currency, + "credit_in_account_currency": bank_transaction.deposit, + "debit_in_account_currency": bank_transaction.withdrawal, + "party_type": party_type, + "party": party, + "cost_center": get_default_cost_center(company), + } - accounts.append( - { - "account": company_account, - "bank_account": bank_transaction.bank_account, - "credit_in_account_currency": bank_transaction.withdrawal, - "debit_in_account_currency": bank_transaction.deposit, - "cost_center": get_default_cost_center(company), - } - ) + company_account_dict = { + "account": company_account, + "account_currency": company_account_currency, + "bank_account": bank_transaction.bank_account, + "credit_in_account_currency": bank_transaction.withdrawal, + "debit_in_account_currency": bank_transaction.deposit, + "cost_center": get_default_cost_center(company), + } + + if is_multi_currency: + exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) + withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal)) + deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit)) + + if second_account_currency != company_default_currency: + exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date) + second_account_dict.update( + { + "exchange_rate": exc_rate, + "credit": deposit_in_company_currency, + "debit": withdrawal_in_company_currency, + } + ) + else: + second_account_dict.update( + { + "exchange_rate": 1, + "credit": deposit_in_company_currency, + "debit": withdrawal_in_company_currency, + "credit_in_account_currency": deposit_in_company_currency, + "debit_in_account_currency": withdrawal_in_company_currency, + } + ) + + if company_account_currency != company_default_currency: + exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) + company_account_dict.update( + { + "exchange_rate": exc_rate, + "credit": withdrawal_in_company_currency, + "debit": deposit_in_company_currency, + } + ) + + accounts.append(second_account_dict) + accounts.append(company_account_dict) journal_entry_dict = { "voucher_type": entry_type, @@ -196,6 +242,9 @@ def create_journal_entry_bts( "cheque_no": reference_number, "mode_of_payment": mode_of_payment, } + if is_multi_currency: + journal_entry_dict.update({"multi_currency": True}) + journal_entry = frappe.new_doc("Journal Entry") journal_entry.update(journal_entry_dict) journal_entry.set("accounts", accounts) From b1b157aa1946d94da34f64321c505b4c0158f3ae Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 25 Oct 2023 09:45:05 +0530 Subject: [PATCH 10/35] refactor: handle bank transaction in foreign currency (cherry picked from commit 74a0d6408a2082a2a039cd55547e56206e7c70bd) --- .../bank_reconciliation_tool.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 867dbdcaa1..0779a09e2f 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -151,7 +151,7 @@ def create_journal_entry_bts( bank_transaction = frappe.db.get_values( "Bank Transaction", bank_transaction_name, - fieldname=["name", "deposit", "withdrawal", "bank_account"], + fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"], as_dict=True, )[0] company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") @@ -169,10 +169,12 @@ def create_journal_entry_bts( company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency") second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency") + # determine if multi-currency Journal or not is_multi_currency = ( True if company_default_currency != company_account_currency or company_default_currency != second_account_currency + or company_default_currency != bank_transaction.currency else False ) @@ -196,11 +198,16 @@ def create_journal_entry_bts( "cost_center": get_default_cost_center(company), } + # convert transaction amount to company currency if is_multi_currency: - exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) + exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date) withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal)) deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit)) + else: + withdrawal_in_company_currency = bank_transaction.withdrawal + deposit_in_company_currency = bank_transaction.deposit + # if second account is of foreign currency, convert and set debit and credit fields. if second_account_currency != company_default_currency: exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date) second_account_dict.update( @@ -208,6 +215,8 @@ def create_journal_entry_bts( "exchange_rate": exc_rate, "credit": deposit_in_company_currency, "debit": withdrawal_in_company_currency, + "credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0, + "debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0, } ) else: @@ -221,6 +230,7 @@ def create_journal_entry_bts( } ) + # if company account is of foreign currency, convert and set debit and credit fields. if company_account_currency != company_default_currency: exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date) company_account_dict.update( @@ -230,6 +240,16 @@ def create_journal_entry_bts( "debit": deposit_in_company_currency, } ) + else: + company_account_dict.update( + { + "exchange_rate": 1, + "credit": withdrawal_in_company_currency, + "debit": deposit_in_company_currency, + "credit_in_account_currency": withdrawal_in_company_currency, + "debit_in_account_currency": deposit_in_company_currency, + } + ) accounts.append(second_account_dict) accounts.append(company_account_dict) From 703be50bc7187fcc7ea203172382d70023b1ce3e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:58:20 +0530 Subject: [PATCH 11/35] fix(ux): don't override Item Name and Description in MR (backport #38720) (#38763) fix(ux): don't override Item Name and Description in MR (cherry picked from commit 726ac6bda1ee3b25c1d62b312d96aa32466ba11e) Co-authored-by: s-aga-r --- erpnext/stock/doctype/material_request/material_request.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 9673a70501..d90b71a47a 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -199,9 +199,8 @@ frappe.ui.form.on('Material Request', { get_item_data: function(frm, item, overwrite_warehouse=false) { if (item && !item.item_code) { return; } - frm.call({ + frappe.call({ method: "erpnext.stock.get_item_details.get_item_details", - child: item, args: { args: { item_code: item.item_code, From bf8a2d0e3aab2790beb071785068df78d64f32ae Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 15 Dec 2023 12:30:15 +0530 Subject: [PATCH 12/35] fix: skip jvs against bank accounts (cherry picked from commit f7b2380ec1cbe5e58755f88ca08cb052b92e05c7) --- .../tax_withholding_details.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index cb0be828f4..9513f2c153 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.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 aa5e16e68134dabd58d95bdcd47db78d6c045319 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Dec 2023 11:46:12 +0530 Subject: [PATCH 13/35] fix: validation error on reconciling PE to Journals as Invoice With the same exchange rate on Journal Entry and Payment Entry, reconcilition should not post exc gain/loss journal and should not throw validation error (cherry picked from commit 5eeb650dfd3ab4b20f49bacefc43d98cb856c3f3) --- erpnext/accounts/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 934fafb820..a34282eef1 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -657,8 +657,10 @@ def update_reference_in_payment_entry( "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), - "exchange_gain_loss": d.exchange_gain_loss, + "exchange_rate": d.exchange_rate + if d.difference_amount is not None + else payment_entry.get_exchange_rate(), + "exchange_gain_loss": d.difference_amount, "account": d.account, } From 6e92c78cbdb1348629d7238bd61e7e3ec7bf0a10 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:47:24 +0530 Subject: [PATCH 14/35] fix: asset patch failure due to missing shift_based column (backport #38776) (#38777) fix: asset patch failure due to missing shift_based column (#38776) * fix: add missing daily_prorata_based in get_asset_finance_books_map * fix: reload Asset Finance Book doctype (cherry picked from commit 1704180f38802ba81e9c912455e74d9a0595233e) Co-authored-by: Anand Baburajan --- .../v15_0/create_asset_depreciation_schedules_from_assets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 793497b766..ddce997d11 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -3,6 +3,7 @@ import frappe def execute(): frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") + frappe.reload_doc("assets", "doctype", "Asset Finance Book") assets = get_details_of_draft_or_submitted_depreciable_assets() @@ -86,6 +87,7 @@ def get_asset_finance_books_map(): afb.frequency_of_depreciation, afb.rate_of_depreciation, afb.expected_value_after_useful_life, + afb.daily_prorata_based, afb.shift_based, ) .where(asset.docstatus < 2) From 7fc8150617c3d6410702871289f80f5dab803f9a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:32:59 +0530 Subject: [PATCH 15/35] fix: wrong currency in Stock Balance report (backport #38778) (#38780) fix: wrong currency in Stock Balance report (cherry picked from commit 5a83a16e60b2c47f8ef0307edcd0311824007c86) Co-authored-by: s-aga-r --- erpnext/stock/report/stock_balance/stock_balance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index a59f9de42e..ed84a5c2d5 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -413,7 +413,7 @@ class StockBalanceReport(object): "fieldname": "bal_val", "fieldtype": "Currency", "width": 100, - "options": "currency", + "options": "Company:company:default_currency", }, { "label": _("Opening Qty"), @@ -427,7 +427,7 @@ class StockBalanceReport(object): "fieldname": "opening_val", "fieldtype": "Currency", "width": 110, - "options": "currency", + "options": "Company:company:default_currency", }, { "label": _("In Qty"), From 7802f6c5283ae57316eec06745f73e91fc89202f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 12 Dec 2023 15:18:54 +0530 Subject: [PATCH 16/35] fix: Init internal child table values (cherry picked from commit 2588970d5576bbfa085a1f30cf098d7e18e71a84) --- erpnext/controllers/accounts_controller.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ae51fbd6ea..674a6ab46a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -166,6 +166,7 @@ class AccountsController(TransactionBase): self.disable_pricing_rule_on_internal_transfer() self.disable_tax_included_prices_for_internal_transfer() self.set_incoming_rate() + self.init_internal_values() if self.meta.get_field("currency"): self.calculate_taxes_and_totals() @@ -225,6 +226,16 @@ class AccountsController(TransactionBase): self.set_total_in_words() + def init_internal_values(self): + # init all the internal values as 0 on sa + if self.docstatus.is_draft(): + # TODO: Add all such pending values here + fields = ["billed_amt", "delivered_qty"] + for item in self.get("items"): + for field in fields: + if hasattr(item, field): + item.set(field, 0) + def before_cancel(self): validate_einvoice_fields(self) From 4ed86dbff2c74958fb967fc45066d056e948dc09 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 14 Dec 2023 12:28:32 +0530 Subject: [PATCH 17/35] fix: show bill_date and bill_no in Purchase Register (cherry picked from commit f53ba178a8b0909373c378db31d01928c576ee89) --- erpnext/accounts/report/purchase_register/purchase_register.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 9721987f89..39eb312e4a 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None): "payable_account": inv.credit_to, "mode_of_payment": inv.mode_of_payment, "project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project, + "bill_no": inv.bill_no, + "bill_date": inv.bill_date, "remarks": inv.remarks, "purchase_order": ", ".join(purchase_order), "purchase_receipt": ", ".join(purchase_receipt), From 77dba4834cbe8645c16da8a8c321552a7a35eb59 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 30 Nov 2023 06:20:15 +0000 Subject: [PATCH 18/35] fix(pe): show split alert only on splitting --- .../accounts/doctype/payment_entry/payment_entry.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 8c31df387a..1282ab6039 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1703,12 +1703,13 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list if not split_rows: continue - frappe.msgprint( - _("Splitting {0} {1} into {2} rows as per Payment Terms").format( - _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) - ), - alert=True, - ) + if len(split_rows) > 1: + frappe.msgprint( + _("Splitting {0} {1} into {2} rows as per Payment Terms").format( + _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows) + ), + alert=True, + ) outstanding_invoices_after_split += split_rows continue From 204530628303551551fb256669181a03cce1d881 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:02:33 +0530 Subject: [PATCH 19/35] fix: Reset SLA on issue doesn't work (backport #38789) (#38791) fix: Reset SLA on issue doesn't work (#38789) This was broken since last refactor where it was spun off to work with all types of doctypes but client side code was never adapted. (cherry picked from commit fa1c7b663c2e3f433190d29017eaebbe15d4c604) Co-authored-by: Ankush Menat --- erpnext/support/doctype/issue/issue.js | 4 +++- .../service_level_agreement/service_level_agreement.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index f96823b290..9f91dc1726 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", { frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", { reason: values.reason, - user: frappe.session.user_email + user: frappe.session.user_email, + doctype: frm.doc.doctype, + docname: frm.doc.name, }, () => { reset_sla.enable_primary_action(); frm.refresh(); diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 879381c3f2..77e102fc8d 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -774,10 +774,12 @@ def get_response_and_resolution_duration(doc): return priority -def reset_service_level_agreement(doc, reason, user): +@frappe.whitelist() +def reset_service_level_agreement(doctype: str, docname: str, reason, user): if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) + doc = frappe.get_doc(doctype, docname) frappe.get_doc( { "doctype": "Comment", From fba28d6941695a2b6f4a5343775b58d9c4f60bf3 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sun, 17 Dec 2023 13:59:22 +0530 Subject: [PATCH 20/35] fix: fetch item_tax_template values if fields with fetch_from exisit --- erpnext/controllers/accounts_controller.py | 1 + erpnext/public/js/controllers/transaction.js | 1 + erpnext/stock/get_item_details.py | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 674a6ab46a..4447b076c7 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -650,6 +650,7 @@ class AccountsController(TransactionBase): args["doctype"] = self.doctype args["name"] = self.name + args["child_doctype"] = item.doctype args["child_docname"] = item.name args["ignore_pricing_rule"] = ( self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 66216186a1..908eec4d7c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -513,6 +513,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe cost_center: item.cost_center, tax_category: me.frm.doc.tax_category, item_tax_template: item.item_tax_template, + child_doctype: item.doctype, child_docname: item.name, is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow, } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 8ccc8c9676..e746595921 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -8,6 +8,7 @@ import frappe from frappe import _, throw from frappe.model import child_table_fields, default_fields from frappe.model.meta import get_field_precision +from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, getdate @@ -574,6 +575,9 @@ def get_item_tax_template(args, item, out): item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_group = item_group_doc.parent_item_group + if args.get("child_doctype") and item_tax_template: + out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template)) + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: From 7d844411fb581d4d74dbeb758b5f16c3e56daf24 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 17 Dec 2023 13:36:58 +0530 Subject: [PATCH 21/35] fix(demo): Demo setup for canadian COA (cherry picked from commit c9fd1822681420868923fd53aa1df3e3c776a4a6) --- ...plan_comptable_pour_les_provinces_francophones.json | 10 +++++++--- erpnext/setup/demo.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json index 2811fc5fb6..2a30cbcbc9 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ca_plan_comptable_pour_les_provinces_francophones.json @@ -33,7 +33,9 @@ }, "Stocks": { "Mati\u00e8res premi\u00e8res": {}, - "Stock de produits fini": {}, + "Stock de produits fini": { + "account_type": "Stock" + }, "Stock exp\u00e9di\u00e9 non-factur\u00e9": {}, "Travaux en cours": {}, "account_type": "Stock" @@ -395,9 +397,11 @@ }, "Produits": { "Revenus de ventes": { - " Escomptes de volume sur ventes": {}, + "Escomptes de volume sur ventes": {}, "Autres produits d'exploitation": {}, - "Ventes": {}, + "Ventes": { + "account_type": "Income Account" + }, "Ventes avec des provinces harmonis\u00e9es": {}, "Ventes avec des provinces non-harmonis\u00e9es": {}, "Ventes \u00e0 l'\u00e9tranger": {} diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py index 926283ff1c..4bc98b91bd 100644 --- a/erpnext/setup/demo.py +++ b/erpnext/setup/demo.py @@ -112,9 +112,9 @@ def create_transaction(doctype, company, start_date): warehouse = get_warehouse(company) if document_type == "Purchase Order": - posting_date = get_random_date(start_date, 1, 30) + posting_date = get_random_date(start_date, 1, 25) else: - posting_date = get_random_date(start_date, 31, 364) + posting_date = get_random_date(start_date, 31, 350) doctype.update( { From 18bd330a590055287f6dc8c509f65452664e45d3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 17:41:51 +0530 Subject: [PATCH 22/35] fix: incorrect available qty for backdated stock reco with batch (backport #37858) (#38811) fix: incorrect available qty for backdated stock reco with batch (#37858) * fix: incorrect available qty for backdated stock reco with batch * test: added test case (cherry picked from commit d4c0dbfacc7e99da6cba2c5d389f0a662490b0eb) Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle.py | 13 +- .../stock_reconciliation.py | 135 +++++++++++++----- .../test_stock_reconciliation.py | 69 ++++++++- .../stock_reconciliation_item.json | 3 +- .../stock/report/stock_ledger/stock_ledger.py | 8 ++ .../stock_ledger_invariant_check.py | 13 +- erpnext/stock/stock_ledger.py | 68 +++++---- 7 files changed, 229 insertions(+), 80 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 28b7dc337b..48002323c2 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -157,7 +157,7 @@ class SerialandBatchBundle(Document): def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) - def set_incoming_rate(self, row=None, save=False): + def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False): if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [ "Installation Note", "Job Card", @@ -167,7 +167,9 @@ class SerialandBatchBundle(Document): return if self.type_of_transaction == "Outward": - self.set_incoming_rate_for_outward_transaction(row, save) + self.set_incoming_rate_for_outward_transaction( + row, save, allow_negative_stock=allow_negative_stock + ) else: self.set_incoming_rate_for_inward_transaction(row, save) @@ -188,7 +190,9 @@ class SerialandBatchBundle(Document): def get_serial_nos(self): return [d.serial_no for d in self.entries if d.serial_no] - def set_incoming_rate_for_outward_transaction(self, row=None, save=False): + def set_incoming_rate_for_outward_transaction( + self, row=None, save=False, allow_negative_stock=False + ): sle = self.get_sle_for_outward_transaction() if self.has_serial_no: @@ -217,7 +221,8 @@ class SerialandBatchBundle(Document): if self.docstatus == 1: available_qty += flt(d.qty) - self.validate_negative_batch(d.batch_no, available_qty) + if not allow_negative_stock: + self.validate_negative_batch(d.batch_no, available_qty) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0482467dca..e8d652e2b2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -6,7 +6,7 @@ from typing import Optional import frappe from frappe import _, bold, msgprint from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, cstr, flt +from frappe.utils import add_to_date, cint, cstr, flt import erpnext from erpnext.accounts.utils import get_company_default @@ -116,9 +116,12 @@ class StockReconciliation(StockController): self.repost_future_sle_and_gle() self.delete_auto_created_batches() - def set_current_serial_and_batch_bundle(self): + def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None: """Set Serial and Batch Bundle for each item""" for item in self.items: + if voucher_detail_no and voucher_detail_no != item.name: + continue + item_details = frappe.get_cached_value( "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 ) @@ -176,6 +179,7 @@ class StockReconciliation(StockController): "warehouse": item.warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, + "ignore_voucher_nos": [self.name], } ) ) @@ -191,11 +195,36 @@ class StockReconciliation(StockController): ) if not serial_and_batch_bundle.entries: + if voucher_detail_no: + return + continue - item.current_serial_and_batch_bundle = serial_and_batch_bundle.save().name + serial_and_batch_bundle.save() + item.current_serial_and_batch_bundle = serial_and_batch_bundle.name item.current_qty = abs(serial_and_batch_bundle.total_qty) item.current_valuation_rate = abs(serial_and_batch_bundle.avg_rate) + if save: + sle_creation = frappe.db.get_value( + "Serial and Batch Bundle", item.serial_and_batch_bundle, "creation" + ) + creation = add_to_date(sle_creation, seconds=-1) + item.db_set( + { + "current_serial_and_batch_bundle": item.current_serial_and_batch_bundle, + "current_qty": item.current_qty, + "current_valuation_rate": item.current_valuation_rate, + "creation": creation, + } + ) + + serial_and_batch_bundle.db_set( + { + "creation": creation, + "voucher_no": self.name, + "voucher_detail_no": voucher_detail_no, + } + ) def set_new_serial_and_batch_bundle(self): for item in self.items: @@ -737,56 +766,84 @@ class StockReconciliation(StockController): else: self._cancel() - def recalculate_current_qty(self, item_code, batch_no): + def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False): from erpnext.stock.stock_ledger import get_valuation_rate sl_entries = [] + for row in self.items: - if ( - not (row.item_code == item_code and row.batch_no == batch_no) - and not row.serial_and_batch_bundle - ): + if voucher_detail_no != row.name: continue + current_qty = 0.0 if row.current_serial_and_batch_bundle: - self.recalculate_qty_for_serial_and_batch_bundle(row) - continue - - current_qty = get_batch_qty_for_stock_reco( - item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name - ) + current_qty = self.get_qty_for_serial_and_batch_bundle(row) + elif row.batch_no: + current_qty = get_batch_qty_for_stock_reco( + row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name + ) precesion = row.precision("current_qty") - if flt(current_qty, precesion) == flt(row.current_qty, precesion): - continue + if flt(current_qty, precesion) != flt(row.current_qty, precesion): + val_rate = get_valuation_rate( + row.item_code, + row.warehouse, + self.doctype, + self.name, + company=self.company, + batch_no=row.batch_no, + serial_and_batch_bundle=row.current_serial_and_batch_bundle, + ) - val_rate = get_valuation_rate( - item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no - ) + row.current_valuation_rate = val_rate + row.current_qty = current_qty + row.db_set( + { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + ) - row.current_valuation_rate = val_rate - if not row.current_qty and current_qty: - sle = self.get_sle_for_items(row) - sle.actual_qty = current_qty * -1 - sle.valuation_rate = val_rate - sl_entries.append(sle) + if ( + add_new_sle + and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, + "name", + ) + and (not row.current_serial_and_batch_bundle and not row.batch_no) + ): + self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) + row.reload() - row.current_qty = current_qty - row.db_set( - { - "current_qty": row.current_qty, - "current_valuation_rate": row.current_valuation_rate, - "current_amount": flt(row.current_qty * row.current_valuation_rate), - } - ) + if row.current_qty > 0 and row.current_serial_and_batch_bundle: + new_sle = self.get_sle_for_items(row) + new_sle.actual_qty = row.current_qty * -1 + new_sle.valuation_rate = row.current_valuation_rate + new_sle.creation_time = add_to_date(sle_creation, seconds=-1) + new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle + new_sle.qty_after_transaction = 0.0 + sl_entries.append(new_sle) if sl_entries: - self.make_sl_entries(sl_entries, allow_negative_stock=True) + self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) + if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): + self.repost_future_sle_and_gle(force=True) - def recalculate_qty_for_serial_and_batch_bundle(self, row): + def has_negative_stock_allowed(self): + allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + + if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items): + allow_negative_stock = True + + return allow_negative_stock + + def get_qty_for_serial_and_batch_bundle(self, row): doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle) precision = doc.entries[0].precision("qty") + current_qty = 0 for d in doc.entries: qty = ( get_batch_qty( @@ -799,10 +856,12 @@ class StockReconciliation(StockController): or 0 ) * -1 - if flt(d.qty, precision) == flt(qty, precision): - continue + if flt(d.qty, precision) != flt(qty, precision): + d.db_set("qty", qty) - d.db_set("qty", qty) + current_qty += qty + + return abs(current_qty) def get_batch_qty_for_stock_reco( diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 34a7cbaa72..1ec99bf9a5 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -742,13 +742,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): se2.cancel() - self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) - - self.assertEqual( - frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"), - "Completed", - ) - sle = frappe.get_all( "Stock Ledger Entry", filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0}, @@ -766,6 +759,68 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sle[0].actual_qty), flt(-100.0)) + def test_backdated_stock_reco_entry_with_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test New Batch Item ABCVSD", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + # Stock Reco for 100, Balace Qty 100 + stock_reco = create_stock_reconciliation( + item_code=item_code, + posting_date=nowdate(), + posting_time="11:00:00", + warehouse=warehouse, + qty=100, + rate=100, + ) + + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["actual_qty"], + filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, + ) + + self.assertEqual(len(sles), 1) + + stock_reco.reload() + batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle) + + # Stock Reco for 100, Balace Qty 100 + stock_reco1 = create_stock_reconciliation( + item_code=item_code, + posting_date=add_days(nowdate(), -1), + posting_time="11:00:00", + batch_no=batch_no, + warehouse=warehouse, + qty=60, + rate=100, + ) + + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["actual_qty"], + filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, + ) + + stock_reco1.reload() + new_batch_no = get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle) + + self.assertEqual(len(sles), 2) + + for row in sles: + if row.actual_qty < 0: + self.assertEqual(row.actual_qty, -60) + def test_update_stock_reconciliation_while_reposting(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index ca19bbb96a..d9cbf95710 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -205,6 +205,7 @@ "fieldname": "current_serial_and_batch_bundle", "fieldtype": "Link", "label": "Current Serial / Batch Bundle", + "no_copy": 1, "options": "Serial and Batch Bundle", "read_only": 1 }, @@ -216,7 +217,7 @@ ], "istable": 1, "links": [], - "modified": "2023-07-26 12:54:34.011915", + "modified": "2023-11-02 15:47:07.929550", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index eeef39641b..e59f2fe644 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -249,6 +249,13 @@ def get_columns(filters): "options": "Serial No", "width": 100, }, + { + "label": _("Serial and Batch Bundle"), + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "options": "Serial and Batch Bundle", + "width": 100, + }, {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, { "label": _("Project"), @@ -287,6 +294,7 @@ def get_stock_ledger_entries(filters, items): sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, + sle.serial_and_batch_bundle, sle.voucher_no, sle.stock_value, sle.batch_no, diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index ca15afe444..fb392f7e36 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -24,6 +24,7 @@ SLE_FIELDS = ( "stock_value_difference", "valuation_rate", "voucher_detail_no", + "serial_and_batch_bundle", ) @@ -64,7 +65,11 @@ def add_invariant_check_fields(sles): balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + if ( + sle.voucher_type == "Stock Reconciliation" + and not sle.batch_no + and not sle.serial_and_batch_bundle + ): balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty") if balance_qty is None: balance_qty = sle.qty_after_transaction @@ -143,6 +148,12 @@ def get_columns(): "label": _("Batch"), "options": "Batch", }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": _("Serial and Batch Bundle"), + "options": "Serial and Batch Bundle", + }, { "fieldname": "use_batchwise_valuation", "fieldtype": "Check", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a53180442f..9142a27f4c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -210,6 +210,11 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.allow_negative_stock = allow_negative_stock sle.via_landed_cost_voucher = via_landed_cost_voucher sle.submit() + + # Added to handle the case when the stock ledger entry is created from the repostig + if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation": + sle.db_set("creation", args.get("creation_time")) + return sle @@ -696,9 +701,11 @@ class update_entries_after(object): if ( sle.voucher_type == "Stock Reconciliation" - and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle)) + and ( + sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no) + ) and sle.voucher_detail_no - and sle.actual_qty < 0 + and not self.args.get("sle_id") ): self.reset_actual_qty_for_stock_reco(sle) @@ -765,27 +772,22 @@ class update_entries_after(object): self.update_outgoing_rate_on_transaction(sle) def reset_actual_qty_for_stock_reco(self, sle): - if sle.serial_and_batch_bundle: - current_qty = frappe.get_cached_value( - "Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty" + doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) + doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) + + if sle.actual_qty < 0: + sle.actual_qty = ( + flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) + * -1 ) - if current_qty is not None: - current_qty = abs(current_qty) - else: - current_qty = frappe.get_cached_value( - "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty" - ) - - if current_qty: - sle.actual_qty = current_qty * -1 - elif current_qty == 0: - sle.is_cancelled = 1 + if abs(sle.actual_qty) == 0.0: + sle.is_cancelled = 1 def calculate_valuation_for_serial_batch_bundle(self, sle): doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) - doc.set_incoming_rate(save=True) + doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock) doc.calculate_qty_and_amount(save=True) self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount) @@ -1472,6 +1474,7 @@ def get_valuation_rate( currency=None, company=None, raise_error_if_no_rate=True, + batch_no=None, serial_and_batch_bundle=None, ): @@ -1480,6 +1483,25 @@ def get_valuation_rate( if not company: company = frappe.get_cached_value("Warehouse", warehouse, "company") + if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"): + table = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(table) + .select(Sum(table.stock_value_difference) / Sum(table.actual_qty)) + .where( + (table.item_code == item_code) + & (table.warehouse == warehouse) + & (table.batch_no == batch_no) + & (table.is_cancelled == 0) + & (table.voucher_no != voucher_no) + & (table.voucher_type != voucher_type) + ) + ) + + last_valuation_rate = query.run() + if last_valuation_rate: + return flt(last_valuation_rate[0][0]) + # Get moving average rate of a specific batch number if warehouse and serial_and_batch_bundle: batch_obj = BatchNoValuation( @@ -1574,8 +1596,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] - if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no): - regenerate_sle_for_batch_stock_reco(detail) # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) @@ -1604,16 +1624,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): validate_negative_qty_in_future_sle(args, allow_negative_stock) -def regenerate_sle_for_batch_stock_reco(detail): - doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no) - doc.recalculate_current_qty(detail.item_code, detail.batch_no) - - if not frappe.db.exists( - "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"} - ): - doc.repost_future_sle_and_gle(force=True) - - def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): From 8990c48e7bf1c556ad234756259d991519769e09 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 18:02:31 +0530 Subject: [PATCH 23/35] fix: serial and batch bundle return not working (backport #38754) (#38806) fix: serial and batch bundle return not working (#38754) * fix: serial and batch bundle return not working * test: added test case for delivery note return against denormalized serial no (cherry picked from commit 0743289925d0866a16373c05cfb81825b58e35ba) Co-authored-by: rohitwaghchaure --- .../controllers/sales_and_purchase_return.py | 191 ++++++++++++------ erpnext/controllers/selling_controller.py | 4 + erpnext/controllers/stock_controller.py | 6 + erpnext/public/js/controllers/buying.js | 14 +- erpnext/public/js/utils/sales_common.js | 7 +- .../js/utils/serial_no_batch_selector.js | 49 +++-- .../delivery_note/test_delivery_note.py | 109 ++++++++++ .../serial_and_batch_bundle.py | 107 +++++++++- .../stock/doctype/serial_no/serial_no.json | 28 +-- erpnext/stock/doctype/serial_no/serial_no.py | 22 +- .../stock_ledger_entry/stock_ledger_entry.py | 3 + erpnext/stock/serial_batch_bundle.py | 53 ++++- 12 files changed, 460 insertions(+), 133 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 81e71e3aa2..81080f0266 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -8,6 +8,8 @@ from frappe.model.meta import get_field_precision from frappe.utils import flt, format_datetime, get_datetime import erpnext +from erpnext.stock.serial_batch_bundle import get_batches_from_bundle +from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle from erpnext.stock.utils import get_incoming_rate @@ -69,8 +71,6 @@ def validate_return_against(doc): def validate_returned_items(doc): - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - valid_items = frappe._dict() select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor" @@ -123,26 +123,6 @@ def validate_returned_items(doc): ) ) - elif ref.batch_no and d.batch_no not in ref.batch_no: - frappe.throw( - _("Row # {0}: Batch No must be same as {1} {2}").format( - d.idx, doc.doctype, doc.return_against - ) - ) - - elif ref.serial_no: - if d.qty and not d.serial_no: - frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx)) - else: - serial_nos = get_serial_nos(d.serial_no) - for s in serial_nos: - if s not in ref.serial_no: - frappe.throw( - _("Row # {0}: Serial No {1} does not match with {2} {3}").format( - d.idx, s, doc.doctype, doc.return_against - ) - ) - if ( warehouse_mandatory and not d.get("warehouse") @@ -397,71 +377,92 @@ def make_return_doc( else: doc.run_method("calculate_taxes_and_totals") - def update_item(source_doc, target_doc, source_parent): + def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.serial_batch_bundle import SerialBatchCreation - target_doc.qty = -1 * source_doc.qty - item_details = frappe.get_cached_value( - "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1 - ) - returned_serial_nos = [] - if source_doc.get("serial_and_batch_bundle"): - if item_details.has_serial_no: - returned_serial_nos = get_returned_serial_nos(source_doc, source_parent) + returned_batches = frappe._dict() + serial_and_batch_field = ( + "serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle" + ) + old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no" + old_batch_no_field = "batch_no" - type_of_transaction = "Inward" - if ( - frappe.db.get_value( - "Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction" - ) - == "Inward" - ): - type_of_transaction = "Outward" - - cls_obj = SerialBatchCreation( - { - "type_of_transaction": type_of_transaction, - "serial_and_batch_bundle": source_doc.serial_and_batch_bundle, - "returned_against": source_doc.name, - "item_code": source_doc.item_code, - "returned_serial_nos": returned_serial_nos, - } - ) - - cls_obj.duplicate_package() - if cls_obj.serial_and_batch_bundle: - target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle - - if source_doc.get("rejected_serial_and_batch_bundle"): + if ( + source_doc.get(serial_and_batch_field) + or source_doc.get(old_serial_no_field) + or source_doc.get(old_batch_no_field) + ): if item_details.has_serial_no: returned_serial_nos = get_returned_serial_nos( - source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle" + source_doc, source_parent, serial_no_field=serial_and_batch_field + ) + else: + returned_batches = get_returned_batches( + source_doc, source_parent, batch_no_field=serial_and_batch_field ) type_of_transaction = "Inward" - if ( + if source_doc.get(serial_and_batch_field) and ( frappe.db.get_value( - "Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction" + "Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction" ) == "Inward" ): type_of_transaction = "Outward" + elif source_parent.doctype in [ + "Purchase Invoice", + "Purchase Receipt", + "Subcontracting Receipt", + ]: + type_of_transaction = "Outward" cls_obj = SerialBatchCreation( { "type_of_transaction": type_of_transaction, - "serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle, + "serial_and_batch_bundle": source_doc.get(serial_and_batch_field), "returned_against": source_doc.name, "item_code": source_doc.item_code, "returned_serial_nos": returned_serial_nos, + "voucher_type": source_parent.doctype, + "do_not_submit": True, + "warehouse": source_doc.warehouse, + "has_serial_no": item_details.has_serial_no, + "has_batch_no": item_details.has_batch_no, } ) - cls_obj.duplicate_package() - if cls_obj.serial_and_batch_bundle: - target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle + serial_nos = [] + batches = frappe._dict() + if source_doc.get(old_batch_no_field): + batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)}) + elif source_doc.get(old_serial_no_field): + serial_nos = get_serial_nos(source_doc.get(old_serial_no_field)) + elif source_doc.get(serial_and_batch_field): + if item_details.has_serial_no: + serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field)) + else: + batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field)) + if serial_nos: + cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos))) + elif batches: + for batch in batches: + if batch in returned_batches: + batches[batch] -= flt(returned_batches.get(batch)) + + cls_obj.batches = batches + + if source_doc.get(serial_and_batch_field): + cls_obj.duplicate_package() + if cls_obj.serial_and_batch_bundle: + target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle) + else: + target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name) + + def update_item(source_doc, target_doc, source_parent): + target_doc.qty = -1 * source_doc.qty if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype @@ -561,6 +562,17 @@ def make_return_doc( if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return + item_details = frappe.get_cached_value( + "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1 + ) + + if not item_details.has_batch_no and not item_details.has_serial_no: + return + + for qty_field in ["stock_qty", "rejected_qty"]: + if target_doc.get(qty_field): + update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field) + def update_terms(source_doc, target_doc, source_parent): target_doc.payment_amount = -source_doc.payment_amount @@ -716,6 +728,9 @@ def get_returned_serial_nos( [parent_doc.doctype, "docstatus", "=", 1], ] + if serial_no_field == "rejected_serial_and_batch_bundle": + filters.append([child_doc.doctype, "rejected_qty", ">", 0]) + # Required for POS Invoice if ignore_voucher_detail_no: filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no]) @@ -723,9 +738,57 @@ def get_returned_serial_nos( ids = [] for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): ids.append(row.get("serial_and_batch_bundle")) - if row.get(old_field): + if row.get(old_field) and not row.get(serial_no_field): serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field))) - serial_nos.extend(get_serial_nos(ids)) + if ids: + serial_nos.extend(get_serial_nos(ids)) return serial_nos + + +def get_returned_batches( + child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None +): + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle + + batches = frappe._dict() + + old_field = "batch_no" + if not batch_no_field: + batch_no_field = "serial_and_batch_bundle" + + return_ref_field = frappe.scrub(child_doc.doctype) + if child_doc.doctype == "Delivery Note Item": + return_ref_field = "dn_detail" + + fields = [ + f"`{'tab' + child_doc.doctype}`.`{batch_no_field}`", + f"`{'tab' + child_doc.doctype}`.`batch_no`", + f"`{'tab' + child_doc.doctype}`.`stock_qty`", + ] + + filters = [ + [parent_doc.doctype, "return_against", "=", parent_doc.name], + [parent_doc.doctype, "is_return", "=", 1], + [child_doc.doctype, return_ref_field, "=", child_doc.name], + [parent_doc.doctype, "docstatus", "=", 1], + ] + + if batch_no_field == "rejected_serial_and_batch_bundle": + filters.append([child_doc.doctype, "rejected_qty", ">", 0]) + + # Required for POS Invoice + if ignore_voucher_detail_no: + filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no]) + + ids = [] + for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): + ids.append(row.get("serial_and_batch_bundle")) + if row.get(old_field) and not row.get(batch_no_field): + batches.setdefault(row.get(old_field), row.get("stock_qty")) + + if ids: + batches.update(get_batches_from_bundle(ids)) + + return batches diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index fdadb30e93..e8bae8cda5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -308,6 +308,8 @@ class SellingController(StockController): "warehouse": p.warehouse or d.warehouse, "item_code": p.item_code, "qty": flt(p.qty), + "serial_no": p.serial_no if self.docstatus == 2 else None, + "batch_no": p.batch_no if self.docstatus == 2 else None, "uom": p.uom, "serial_and_batch_bundle": p.serial_and_batch_bundle or get_serial_and_batch_bundle(p, self), @@ -330,6 +332,8 @@ class SellingController(StockController): "warehouse": d.warehouse, "item_code": d.item_code, "qty": d.stock_qty, + "serial_no": d.serial_no if self.docstatus == 2 else None, + "batch_no": d.batch_no if self.docstatus == 2 else None, "uom": d.uom, "stock_uom": d.stock_uom, "conversion_factor": d.conversion_factor, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fc45c7ad52..fd417f3270 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -455,6 +455,12 @@ class StockController(AccountsController): sl_dict.update(args) self.update_inventory_dimensions(d, sl_dict) + if self.docstatus == 2: + # To handle denormalized serial no records, will br deprecated in v16 + for field in ["serial_no", "batch_no"]: + if d.get(field): + sl_dict[field] = d.get(field) + return sl_dict def update_inventory_dimensions(self, row, sl_dict) -> None: diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 3ed7fc75cf..77ecf75e0c 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -361,9 +361,14 @@ erpnext.buying = { new erpnext.SerialBatchPackageSelector( me.frm, item, (r) => { if (r) { + let qty = Math.abs(r.total_qty); + if (doc.is_return) { + qty = qty * -1; + } + let update_values = { "serial_and_batch_bundle": r.name, - "qty": Math.abs(r.total_qty) + "qty": qty } if (r.warehouse) { @@ -396,9 +401,14 @@ erpnext.buying = { new erpnext.SerialBatchPackageSelector( me.frm, item, (r) => { if (r) { + let qty = Math.abs(r.total_qty); + if (doc.is_return) { + qty = qty * -1; + } + let update_values = { "serial_and_batch_bundle": r.name, - "rejected_qty": Math.abs(r.total_qty) + "rejected_qty": qty } if (r.warehouse) { diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 5514963c96..084cca7db5 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -317,9 +317,14 @@ erpnext.sales_common = { new erpnext.SerialBatchPackageSelector( me.frm, item, (r) => { if (r) { + let qty = Math.abs(r.total_qty); + if (doc.is_return) { + qty = qty * -1; + } + frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, - "qty": Math.abs(r.total_qty) + "qty": qty }); } } diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 7b9cdfef2a..4abc8fa395 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -32,22 +32,39 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { }); this.dialog.show(); - - let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty; - this.dialog.set_value("qty", qty).then(() => { - if (this.item.serial_no) { - this.dialog.set_value("scan_serial_no", this.item.serial_no); - frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', ''); - } else if (this.item.batch_no) { - this.dialog.set_value("scan_batch_no", this.item.batch_no); - frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', ''); - } - - this.dialog.fields_dict.entries.grid.refresh(); - }); - this.$scan_btn = this.dialog.$wrapper.find(".link-btn"); this.$scan_btn.css("display", "inline"); + + let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty; + + if (this.item?.is_rejected) { + qty = this.item.rejected_qty; + } + + qty = Math.abs(qty); + if (qty > 0) { + this.dialog.set_value("qty", qty).then(() => { + if (this.item.serial_no && !this.item.serial_and_batch_bundle) { + let serial_nos = this.item.serial_no.split('\n'); + if (serial_nos.length > 1) { + serial_nos.forEach(serial_no => { + this.dialog.fields_dict.entries.df.data.push({ + serial_no: serial_no, + batch_no: this.item.batch_no + }); + }); + } else { + this.dialog.set_value("scan_serial_no", this.item.serial_no); + } + frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', ''); + } else if (this.item.batch_no && !this.item.serial_and_batch_bundle) { + this.dialog.set_value("scan_batch_no", this.item.batch_no); + frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', ''); + } + + this.dialog.fields_dict.entries.grid.refresh(); + }); + } } get_serial_no_filters() { @@ -467,13 +484,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } render_data() { - if (!this.frm.is_new() && this.bundle) { + if (this.bundle) { frappe.call({ method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers', args: { item_code: this.item.item_code, name: this.bundle, - voucher_no: this.item.parent, + voucher_no: !this.frm.is_new() ? this.item.parent : "", } }).then(r => { if (r.message) { diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 94655747e4..3a581226ca 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -174,6 +174,115 @@ class TestDeliveryNote(FrappeTestCase): for field, value in field_values.items(): self.assertEqual(cstr(serial_no.get(field)), value) + def test_delivery_note_return_against_denormalized_serial_no(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + frappe.flags.ignore_serial_batch_bundle_validation = True + sn_item = "Old Serial NO Item Return Test - 1" + make_item( + sn_item, + { + "has_serial_no": 1, + "serial_no_series": "OSN-.####", + "is_stock_item": 1, + }, + ) + + frappe.flags.ignore_serial_batch_bundle_validation = True + serial_nos = [ + "OSN-1", + "OSN-2", + "OSN-3", + "OSN-4", + "OSN-5", + "OSN-6", + "OSN-7", + "OSN-8", + "OSN-9", + "OSN-10", + "OSN-11", + "OSN-12", + ] + + for sn in serial_nos: + if not frappe.db.exists("Serial No", sn): + sn_doc = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": sn_item, + "serial_no": sn, + } + ) + sn_doc.insert() + + warehouse = "_Test Warehouse - _TC" + company = frappe.db.get_value("Warehouse", warehouse, "company") + se_doc = make_stock_entry( + item_code=sn_item, + company=company, + target="_Test Warehouse - _TC", + qty=12, + basic_rate=100, + do_not_submit=1, + ) + + se_doc.items[0].serial_no = "\n".join(serial_nos) + se_doc.submit() + + self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos)) + + dn = create_delivery_note( + item_code=sn_item, + qty=12, + rate=500, + warehouse=warehouse, + company=company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + do_not_submit=1, + ) + + dn.items[0].serial_no = "\n".join(serial_nos) + dn.submit() + dn.reload() + + self.assertTrue(dn.items[0].serial_no) + + frappe.flags.ignore_serial_batch_bundle_validation = False + + # return entry + dn1 = make_sales_return(dn.name) + + dn1.items[0].qty = -2 + + bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle) + bundle_doc.set("entries", bundle_doc.entries[:2]) + bundle_doc.save() + + dn1.save() + dn1.submit() + + returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle) + for serial_no in returned_serial_nos1: + self.assertTrue(serial_no in serial_nos) + + dn2 = make_sales_return(dn.name) + + dn2.items[0].qty = -2 + + bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle) + bundle_doc.set("entries", bundle_doc.entries[:2]) + bundle_doc.save() + + dn2.save() + dn2.submit() + + returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle) + for serial_no in returned_serial_nos2: + self.assertTrue(serial_no in serial_nos) + self.assertFalse(serial_no in returned_serial_nos1) + def test_sales_return_for_non_bundled_items_partial(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 48002323c2..e8c1124d9a 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -23,7 +23,11 @@ from frappe.utils import ( ) from frappe.utils.csvutils import build_csv_response -from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation +from erpnext.stock.serial_batch_bundle import ( + BatchNoValuation, + SerialNoValuation, + get_batches_from_bundle, +) from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle @@ -123,6 +127,11 @@ class SerialandBatchBundle(Document): ) def validate_serial_nos_duplicate(self): + # Don't inward same serial number multiple times + + if not self.warehouse: + return + if self.voucher_type in ["Stock Reconciliation", "Stock Entry"] and self.docstatus != 1: return @@ -146,7 +155,6 @@ class SerialandBatchBundle(Document): kwargs["voucher_no"] = self.voucher_no available_serial_nos = get_available_serial_nos(kwargs) - for data in available_serial_nos: if data.serial_no in serial_nos: self.throw_error_message( @@ -327,6 +335,19 @@ class SerialandBatchBundle(Document): ): values_to_set["posting_time"] = parent.posting_time + if parent.doctype in [ + "Delivery Note", + "Purchase Receipt", + "Purchase Invoice", + "Sales Invoice", + ] and parent.get("is_return"): + return_ref_field = frappe.scrub(parent.doctype) + "_item" + if parent.doctype == "Delivery Note": + return_ref_field = "dn_detail" + + if row.get(return_ref_field): + values_to_set["returned_against"] = row.get(return_ref_field) + if values_to_set: self.db_set(values_to_set) @@ -509,7 +530,6 @@ class SerialandBatchBundle(Document): batch_nos = [] serial_batches = {} - for row in self.entries: if self.has_serial_no and not row.serial_no: frappe.throw( @@ -590,6 +610,67 @@ class SerialandBatchBundle(Document): f"Batch Nos {bold(incorrect_batch_nos)} does not belong to Item {bold(self.item_code)}" ) + def validate_serial_and_batch_no_for_returned(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + if not self.returned_against: + return + + if self.voucher_type not in [ + "Purchase Receipt", + "Purchase Invoice", + "Sales Invoice", + "Delivery Note", + ]: + return + + data = self.get_orignal_document_data() + if not data: + return + + serial_nos, batches = [], [] + current_serial_nos = [d.serial_no for d in self.entries if d.serial_no] + current_batches = [d.batch_no for d in self.entries if d.batch_no] + + for d in data: + if self.has_serial_no: + if d.serial_and_batch_bundle: + serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) + else: + serial_nos = get_serial_nos(d.serial_no) + + elif self.has_batch_no: + if d.serial_and_batch_bundle: + batches = get_batches_from_bundle(d.serial_and_batch_bundle) + else: + batches = frappe._dict({d.batch_no: d.stock_qty}) + + if batches: + batches = [d for d in batches if batches[d] > 0] + + if serial_nos: + if not set(current_serial_nos).issubset(set(serial_nos)): + self.throw_error_message( + f"Serial Nos {bold(', '.join(serial_nos))} are not part of the original document." + ) + + if batches: + if not set(current_batches).issubset(set(batches)): + self.throw_error_message( + f"Batch Nos {bold(', '.join(batches))} are not part of the original document." + ) + + def get_orignal_document_data(self): + fields = ["serial_and_batch_bundle", "stock_qty"] + if self.has_serial_no: + fields.append("serial_no") + + elif self.has_batch_no: + fields.append("batch_no") + + child_doc = self.voucher_type + " Item" + return frappe.get_all(child_doc, fields=fields, filters={"name": self.returned_against}) + def validate_duplicate_serial_and_batch_no(self): serial_nos = [] batch_nos = [] @@ -688,9 +769,29 @@ class SerialandBatchBundle(Document): for batch in batches: frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None}) + def before_submit(self): + self.validate_serial_and_batch_no_for_returned() + self.set_purchase_document_no() + def on_submit(self): self.validate_serial_nos_inventory() + def set_purchase_document_no(self): + if not self.has_serial_no: + return + + if self.total_qty > 0: + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + sn_table = frappe.qb.DocType("Serial No") + ( + frappe.qb.update(sn_table) + .set( + sn_table.purchase_document_no, + self.voucher_no if not sn_table.purchase_document_no else self.voucher_no, + ) + .where(sn_table.name.isin(serial_nos)) + ).run() + def validate_serial_and_batch_inventory(self): self.check_future_entries_exists() self.validate_batch_inventory() diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index b4ece00fe6..2d7fcac89a 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -27,8 +27,6 @@ "column_break_24", "location", "employee", - "delivery_details", - "delivery_document_type", "warranty_amc_details", "column_break6", "warranty_expiry_date", @@ -39,7 +37,8 @@ "more_info", "company", "column_break_2cmm", - "work_order" + "work_order", + "purchase_document_no" ], "fields": [ { @@ -153,20 +152,6 @@ "options": "Employee", "read_only": 1 }, - { - "fieldname": "delivery_details", - "fieldtype": "Section Break", - "label": "Delivery Details", - "oldfieldtype": "Column Break" - }, - { - "fieldname": "delivery_document_type", - "fieldtype": "Link", - "label": "Delivery Document Type", - "no_copy": 1, - "options": "DocType", - "read_only": 1 - }, { "fieldname": "warranty_amc_details", "fieldtype": "Section Break", @@ -275,12 +260,19 @@ { "fieldname": "column_break_2cmm", "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_document_no", + "fieldtype": "Data", + "label": "Creation Document No", + "no_copy": 1, + "read_only": 1 } ], "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2023-11-28 15:37:59.489945", + "modified": "2023-12-17 10:52:55.767839", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index d562560da1..122664c2dd 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -41,7 +41,6 @@ class SerialNo(StockController): batch_no: DF.Link | None brand: DF.Link | None company: DF.Link - delivery_document_type: DF.Link | None description: DF.Text | None employee: DF.Link | None item_code: DF.Link @@ -51,6 +50,7 @@ class SerialNo(StockController): maintenance_status: DF.Literal[ "", "Under Warranty", "Out of Warranty", "Under AMC", "Out of AMC" ] + purchase_document_no: DF.Data | None purchase_rate: DF.Float serial_no: DF.Data status: DF.Literal["", "Active", "Inactive", "Delivered", "Expired"] @@ -231,26 +231,6 @@ def auto_fetch_serial_number( return sorted([d.get("name") for d in serial_numbers]) -def get_delivered_serial_nos(serial_nos): - """ - Returns serial numbers that delivered from the list of serial numbers - """ - from frappe.query_builder.functions import Coalesce - - SerialNo = frappe.qb.DocType("Serial No") - serial_nos = get_serial_nos(serial_nos) - query = ( - frappe.qb.select(SerialNo.name) - .from_(SerialNo) - .where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != "")) - ) - - result = query.run() - if result and len(result) > 0: - delivered_serial_nos = [row[0] for row in result] - return delivered_serial_nos - - @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index e62f0b2ac7..ab39adee5c 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -181,6 +181,9 @@ class StockLedgerEntry(Document): frappe.throw(_("Actual Qty is mandatory")) def validate_serial_batch_no_bundle(self): + if self.is_cancelled == 1: + return + item_detail = frappe.get_cached_value( "Item", self.item_code, diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 0c187923e3..a1874b84dc 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -218,15 +218,16 @@ class SerialBatchBundle: ).validate_serial_and_batch_inventory() def post_process(self): - if not self.sle.serial_and_batch_bundle: + if not self.sle.serial_and_batch_bundle and not self.sle.serial_no and not self.sle.batch_no: return - docstatus = frappe.get_cached_value( - "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus" - ) + if self.sle.serial_and_batch_bundle: + docstatus = frappe.get_cached_value( + "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus" + ) - if docstatus != 1: - self.submit_serial_and_batch_bundle() + if docstatus != 1: + self.submit_serial_and_batch_bundle() if self.item_details.has_serial_no == 1: self.set_warehouse_and_status_in_serial_nos() @@ -249,7 +250,12 @@ class SerialBatchBundle: doc.submit() def set_warehouse_and_status_in_serial_nos(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos + serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle) + if not self.sle.serial_and_batch_bundle and self.sle.serial_no: + serial_nos = get_parsed_serial_nos(self.sle.serial_no) + warehouse = self.warehouse if self.sle.actual_qty > 0 else None if not serial_nos: @@ -263,7 +269,14 @@ class SerialBatchBundle: ( frappe.qb.update(sn_table) .set(sn_table.warehouse, warehouse) - .set(sn_table.status, "Active" if warehouse else status) + .set( + sn_table.status, + "Active" + if warehouse + else status + if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1) + else "Inactive", + ) .where(sn_table.name.isin(serial_nos)) ).run() @@ -290,6 +303,8 @@ class SerialBatchBundle: from erpnext.stock.doctype.batch.batch import get_available_batches batches = get_batch_nos(self.sle.serial_and_batch_bundle) + if not self.sle.serial_and_batch_bundle and self.sle.batch_no: + batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty}) batches_qty = get_available_batches( frappe._dict( @@ -312,13 +327,35 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None): if serial_nos: filters["serial_no"] = ("in", serial_nos) - entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters) + entries = frappe.get_all( + "Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx" + ) if not entries: return [] return [d.serial_no for d in entries if d.serial_no] +def get_batches_from_bundle(serial_and_batch_bundle, batches=None): + if not serial_and_batch_bundle: + return [] + + filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")} + if isinstance(serial_and_batch_bundle, list): + filters = {"parent": ("in", serial_and_batch_bundle)} + + if batches: + filters["batch_no"] = ("in", batches) + + entries = frappe.get_all( + "Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1 + ) + if not entries: + return frappe._dict({}) + + return frappe._dict(entries) + + def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None): return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos) From 58de9913b90ec0d7e3b042d4ea0e39184c4d3694 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 24/35] 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 741d4283e2..daf2e21d78 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 2bf55cfcd0..000ef80ee3 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 5a807af50521b3b96f89c865b943989160c5f32f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 18 Dec 2023 13:39:26 +0530 Subject: [PATCH 25/35] refactor: ignore ERR journals in Statment of Accounts (cherry picked from commit 39ef75e2d0f18aa8d7434297be596b9223aa7910) --- .../process_statement_of_accounts.json | 9 ++++++++- .../process_statement_of_accounts.py | 15 +++++++++++++++ .../report/general_ledger/general_ledger.py | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index a3a953f910..ae6059c005 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -15,6 +15,7 @@ "group_by", "cost_center", "territory", + "ignore_exchange_rate_revaluation_journals", "column_break_14", "to_date", "finance_book", @@ -376,10 +377,16 @@ "fieldname": "pdf_name", "fieldtype": "Data", "label": "PDF Name" + }, + { + "default": "0", + "fieldname": "ignore_exchange_rate_revaluation_journals", + "fieldtype": "Check", + "label": "Ignore Exchange Rate Revaluation Journals" } ], "links": [], - "modified": "2023-08-28 12:59:53.071334", + "modified": "2023-12-18 12:20:08.965120", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 9ad25483e3..c03b18a871 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document): frequency: DF.Literal["Weekly", "Monthly", "Quarterly"] from_date: DF.Date | None group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"] + ignore_exchange_rate_revaluation_journals: DF.Check include_ageing: DF.Check include_break: DF.Check letter_head: DF.Link | None @@ -119,6 +120,18 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" + err_journals = None + if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals: + err_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": doc.company, + "docstatus": 1, + "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), + }, + as_list=True, + ) + for entry in doc.customers: if doc.include_ageing: ageing = set_ageing(doc, entry) @@ -131,6 +144,8 @@ def get_statement_dict(doc, get_statement_dict=False): ) filters = get_common_filters(doc) + if err_journals: + filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) if doc.report == "General Ledger": filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index ac06a1227e..896c4c9800 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -238,6 +238,9 @@ def get_conditions(filters): if filters.get("voucher_no"): conditions.append("voucher_no=%(voucher_no)s") + if filters.get("voucher_no_not_in"): + conditions.append("voucher_no not in %(voucher_no_not_in)s") + if filters.get("group_by") == "Group by Party" and not filters.get("party_type"): conditions.append("party_type in ('Customer', 'Supplier')") From 6ad75e72e6e1142e9414de364f87bf152255fbfb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:09:42 +0530 Subject: [PATCH 26/35] perf: index `return_against` on delivery note (backport #38827) (#38832) perf: index `return_against` on delivery note (#38827) There's a multi-column index but that's useful IFF all parts of column are part of query. return against on it's own is VERY unique because it's a primary key, we don't need a multi-column index here. (cherry picked from commit 8d79365e0ddef42e788f97c668a64c479a803617) Co-authored-by: Ankush Menat --- erpnext/patches.txt | 1 + .../doctype/delivery_note/delivery_note.json | 5 +++-- .../stock/doctype/delivery_note/delivery_note.py | 4 ---- .../doctype/delivery_note/patches/__init__.py | 0 .../patches/drop_unused_return_against_index.py | 15 +++++++++++++++ .../purchase_receipt/purchase_receipt.json | 5 +++-- .../doctype/purchase_receipt/purchase_receipt.py | 4 ---- 7 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 erpnext/stock/doctype/delivery_note/patches/__init__.py create mode 100644 erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b6d4003f4a..5df11d21ab 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,3 +352,4 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") # 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 diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index b85f296d0b..7873d3e6de 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -301,7 +301,8 @@ "no_copy": 1, "options": "Delivery Note", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "collapsible": 1, @@ -1401,7 +1402,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-09-04 14:15:28.363184", + "modified": "2023-12-18 17:19:39.368239", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a101bdf244..675f8e9158 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1294,7 +1294,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): ) return doclist - - -def on_doctype_update(): - frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/delivery_note/patches/__init__.py b/erpnext/stock/doctype/delivery_note/patches/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..8fe4ffb58f --- /dev/null +++ b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py @@ -0,0 +1,15 @@ +import frappe + + +def execute(): + """Drop unused return_against index""" + + 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`" + ) + except Exception: + frappe.log_error("Failed to drop unused index") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index c7ad660497..a181022121 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -289,7 +289,8 @@ "no_copy": 1, "options": "Purchase Receipt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "section_addresses", @@ -1251,7 +1252,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2023-11-28 13:14:15.243474", + "modified": "2023-12-18 17:26:41.279663", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index ab0727163e..23956ce0b7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1357,10 +1357,6 @@ def get_item_account_wise_additional_cost(purchase_document): return item_account_wise_cost -def on_doctype_update(): - frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) - - @erpnext.allow_regional def update_regional_gl_entries(gl_list, doc): return From 4bd1a5f955b94b850b06f385ef70e6943b9725c9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:13:15 +0530 Subject: [PATCH 27/35] fix: not able to cancel SCR with Batch (backport #38817) (backport #38821) (#38829) fix: not able to cancel SCR with Batch (backport #38817) (#38821) * fix: not able to cancel SCR with Batch (#38817) (cherry picked from commit fb5090fd3f23ada507fe8abc5a899f4b06e48d7e) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py * chore: fix test case * chore: fix test case * chore: fix test case * chore: fix test case --------- Co-authored-by: rohitwaghchaure (cherry picked from commit 71e833c3f2323873953c3ad7ab4a9403ebbdeb0a) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../serial_and_batch_bundle.py | 2 +- .../subcontracting_receipt.py | 2 +- .../test_subcontracting_receipt.py | 85 +++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index e8c1124d9a..7ddf1573de 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -459,7 +459,7 @@ class SerialandBatchBundle(Document): qty_field = "qty" precision = row.precision - if self.voucher_type in ["Subcontracting Receipt"]: + if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]: qty_field = "consumed_qty" if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index ac845d86fb..fc1b697a8e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -167,13 +167,13 @@ class SubcontractingReceipt(SubcontractingController): ) self.update_status_updater_args() self.update_prevdoc_status() - self.delete_auto_created_batches() self.set_consumed_qty_in_subcontract_order() self.set_subcontracting_order_status() self.update_stock_ledger() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() self.update_status() + self.delete_auto_created_batches() def validate_items_qty(self): for item in self.items: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 1d007fe3c8..5523c318a5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -953,6 +953,91 @@ class TestSubcontractingReceipt(FrappeTestCase): scr.submit() + def test_subcontracting_receipt_cancel_with_batch(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + # Step - 1: Set Backflush Based On as "BOM" + set_backflush_based_on("BOM") + + # Step - 2: Create FG and RM Items + fg_item = make_item( + properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1} + ).name + rm_item1 = make_item(properties={"is_stock_item": 1}).name + rm_item2 = make_item(properties={"is_stock_item": 1}).name + make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0}) + + # Step - 3: Create BOM for FG Item + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]) + for rm_item in bom.items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + bom = bom.name + + # Step - 4: Create PO and SCO + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item Test For Batch 1", + "qty": 100, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 100, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + for rm_item in sco.supplied_items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + + # Step - 5: Inward Raw Materials + rm_items = get_rm_items(sco.supplied_items) + for rm_item in rm_items: + rm_item["rate"] = 100 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + # Step - 6: Transfer RM's to Subcontractor + se = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + for item in se.items: + self.assertEqual(item.qty, 100) + self.assertEqual(item.basic_rate, 100) + self.assertEqual(item.amount, item.qty * item.basic_rate) + + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "item": fg_item, + "batch_id": frappe.generate_hash(length=10), + } + ).insert(ignore_permissions=True) + + serial_batch_bundle = frappe.get_doc( + { + "doctype": "Serial and Batch Bundle", + "item_code": fg_item, + "warehouse": sco.items[0].warehouse, + "has_batch_no": 1, + "type_of_transaction": "Inward", + "voucher_type": "Subcontracting Receipt", + "entries": [{"batch_no": batch_doc.name, "qty": 100}], + } + ).insert(ignore_permissions=True) + + # Step - 7: Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.items[0].serial_and_batch_bundle = serial_batch_bundle.name + scr.save() + scr.submit() + scr.load_from_db() + + # Step - 8: Cancel Subcontracting Receipt + scr.cancel() + self.assertTrue(scr.docstatus == 2) + @change_settings("Buying Settings", {"auto_create_purchase_receipt": 1}) def test_auto_create_purchase_receipt(self): fg_item = "Subcontracted Item SA1" From 02ceee6669b99f29342a0106abf3d82639a3e1aa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:31:28 +0530 Subject: [PATCH 28/35] fix: not able to make inter-company po from so (backport #38826) (backport #38828) (#38833) fix: not able to make inter-company po from so (backport #38826) (#38828) fix: not able to make inter-company po from so (#38826) (cherry picked from commit 23042dfc3c0d02374c5710ed679731b1910f9b9a) Co-authored-by: rohitwaghchaure (cherry picked from commit 32a608f94848564806d6254d11c6c0655fbcaa9a) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ++++++++++- erpnext/controllers/buying_controller.py | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 96a557b7e8..aa6daa7d42 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2356,9 +2356,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def get_received_items(reference_name, doctype, reference_fieldname): + reference_field = "inter_company_invoice_reference" + if doctype == "Purchase Order": + reference_field = "inter_company_order_reference" + + filters = { + reference_field: reference_name, + "docstatus": 1, + } + target_doctypes = frappe.get_all( doctype, - filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, + filters=filters, as_list=True, ) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3d863e9b87..572fa519e1 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -381,7 +381,11 @@ class BuyingController(SubcontractingController): rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) else: - field = "incoming_rate" if self.get("is_internal_supplier") else "rate" + field = ( + "incoming_rate" + if self.get("is_internal_supplier") and not self.doctype == "Purchase Order" + else "rate" + ) rate = flt( frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) * (d.conversion_factor or 1), From cff9e471620c224133b12e7fe3cc9955e88efcef Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 16 Dec 2023 04:35:43 +0000 Subject: [PATCH 29/35] fix: wrong paid and cn amount on pos invoice (cherry picked from commit 5cb5e09dbbac878906023c07423d5d8233279790) --- .../report/accounts_receivable/accounts_receivable.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 6a9f952bfd..3a196b5187 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -242,8 +242,12 @@ class ReceivablePayableReport(object): row.invoiced_in_account_currency += amount_in_account_currency else: if self.is_invoice(ple): - row.credit_note -= amount - row.credit_note_in_account_currency -= amount_in_account_currency + if row.voucher_no == ple.voucher_no == ple.against_voucher_no: + row.paid -= amount + row.paid_in_account_currency -= amount_in_account_currency + else: + row.credit_note -= amount + row.credit_note_in_account_currency -= amount_in_account_currency else: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency From ffb6d65910041de033d11276f171ae60f9cfe240 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Sat, 16 Dec 2023 05:34:43 +0000 Subject: [PATCH 30/35] test: partial payment for pos invoice (cherry picked from commit 877262891235b21447c5b74684fb7173910427e1) --- .../test_accounts_receivable.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index f83285a1a7..77f8c6eaaa 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): return credit_note + def test_pos_receivable(self): + filters = { + "company": self.company, + "party_type": "Customer", + "party": [self.customer], + "report_date": add_days(today(), 2), + "based_on_payment_terms": 0, + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_remarks": False, + } + + pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + pos_inv.posting_date = add_days(today(), 2) + pos_inv.is_pos = 1 + pos_inv.append( + "payments", + frappe._dict( + mode_of_payment="Cash", + amount=flt(pos_inv.grand_total / 2), + ), + ) + pos_inv.disable_rounded_total = 1 + pos_inv.save() + pos_inv.submit() + + report = execute(filters) + expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]] + + row = report[1][-1] + self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note]) + pos_inv.cancel() + def test_accounts_receivable(self): filters = { "company": self.company, From 7320440b615e9d9c7a53cf5d4ea3989c49999b96 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:17:54 +0530 Subject: [PATCH 31/35] fix: item variant with manufacturer (backport #38845) (backport #38847) (#38851) fix: item variant with manufacturer (backport #38845) (#38847) * fix: item variant with manufacturer (#38845) (cherry picked from commit e0c8ff10daeed0829266aea9142805f68ceedb2b) * chore: fix test case --------- Co-authored-by: rohitwaghchaure (cherry picked from commit 4aa960b744c289b0cda9acfae7911b1ef6ffe5d6) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- erpnext/controllers/item_variant.py | 20 +++++++++-- erpnext/stock/doctype/item/test_item.py | 44 +++++++++---------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index c8785a5a72..ea7fb23cb6 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -56,10 +56,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part copy_attributes_to_variant(template, variant) - variant.manufacturer = manufacturer - variant.manufacturer_part_no = manufacturer_part_no - variant.item_code = append_number_if_name_exists("Item", template.name) + variant.flags.ignore_mandatory = True + variant.save() + + if not frappe.db.exists( + "Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer} + ): + manufacturer_doc = frappe.new_doc("Item Manufacturer") + manufacturer_doc.update( + { + "item_code": variant.name, + "manufacturer": manufacturer, + "manufacturer_part_no": manufacturer_part_no, + } + ) + + manufacturer_doc.flags.ignore_mandatory = True + manufacturer_doc.save(ignore_permissions=True) return variant diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index a942f58bd6..b237f73026 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -522,39 +522,25 @@ class TestItem(FrappeTestCase): self.assertEqual(factor, 1.0) def test_item_variant_by_manufacturer(self): - fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}] - set_item_variant_settings(fields) + template = make_item( + "_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"} + ).name - if frappe.db.exists("Item", "_Test Variant Mfg"): - frappe.delete_doc("Item", "_Test Variant Mfg") - if frappe.db.exists("Item", "_Test Variant Mfg-1"): - frappe.delete_doc("Item", "_Test Variant Mfg-1") - if frappe.db.exists("Manufacturer", "MSG1"): - frappe.delete_doc("Manufacturer", "MSG1") + for manufacturer in ["DFSS", "DASA", "ASAAS"]: + if not frappe.db.exists("Manufacturer", manufacturer): + m_doc = frappe.new_doc("Manufacturer") + m_doc.short_name = manufacturer + m_doc.insert() - template = frappe.get_doc( - dict( - doctype="Item", - item_code="_Test Variant Mfg", - has_variant=1, - item_group="Products", - variant_based_on="Manufacturer", - ) - ).insert() + self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"})) + variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123") - manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert() + item_manufacturer = frappe.db.exists( + "Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name} + ) + self.assertTrue(item_manufacturer) - variant = get_variant(template.name, manufacturer=manufacturer.name) - self.assertEqual(variant.item_code, "_Test Variant Mfg-1") - self.assertEqual(variant.description, "_Test Variant Mfg") - self.assertEqual(variant.manufacturer, "MSG1") - variant.insert() - - variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007") - self.assertEqual(variant.item_code, "_Test Variant Mfg-2") - self.assertEqual(variant.description, "_Test Variant Mfg") - self.assertEqual(variant.manufacturer, "MSG1") - self.assertEqual(variant.manufacturer_part_no, "007") + frappe.delete_doc("Item Manufacturer", item_manufacturer) def test_stock_exists_against_template_item(self): stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1) From cbbc6af128104eef9ff1ef7cc576b518be9f772b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 18:18:40 +0530 Subject: [PATCH 32/35] fix: on closed unreserved the production plan qty (backport #38848) (backport #38859) (#38862) fix: on closed unreserved the production plan qty (backport #38848) (#38859) fix: on closed unreserved the production plan qty (#38848) (cherry picked from commit 2184e8ef58379f53ef8f1d069afa26e64796b073) Co-authored-by: rohitwaghchaure (cherry picked from commit 5e68b7e3a6cea0001ad1275300a019c0db9e8e13) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../production_plan/production_plan.py | 4 ++ .../production_plan/test_production_plan.py | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 13ae3b327a..a00dd084ce 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -583,6 +583,7 @@ class ProductionPlan(Document): if close: self.db_set("status", "Closed") + self.update_bin_qty() return if self.total_produced_qty > 0: @@ -597,6 +598,9 @@ class ProductionPlan(Document): if close is not None: self.db_set("status", self.status) + if self.docstatus == 1 and self.status != "Completed": + self.update_bin_qty() + def update_ordered_status(self): update_status = False for d in self.po_items: diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index cc9d9a0311..f86725d601 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1458,6 +1458,47 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(row.get("uom"), "Nos") self.assertEqual(row.get("conversion_factor"), 10.0) + def test_unreserve_qty_on_closing_of_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, "stock_uom": "_Test UOM 1"}).name + rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + + store_warehouse = create_warehouse("Store Warehouse", company="_Test Company") + 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, stock_uom="_Test UOM 1", do_not_submit=1 + ) + + pln.for_warehouse = rm_warehouse + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + pln.append("mr_items", d) + + pln.save() + pln.submit() + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln.reload() + pln.set_status(close=True) + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertAlmostEqual(after_qty, before_qty - 10) + + pln.reload() + pln.set_status(close=False) + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertAlmostEqual(after_qty, before_qty) + def create_production_plan(**args): """ From 5ec75fb6df592549ec4ec611250007fb631f0f52 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 18:30:36 +0530 Subject: [PATCH 33/35] fix: if not budget then don't validate (backport #38861) (#38865) fix: if not budget then don't validate (#38861) (cherry picked from commit d375164100158db9b974742caa3e05062c481d7d) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/material_request/material_request.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 7df74f81ab..7e34f66c2b 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -169,7 +169,9 @@ class MaterialRequest(BuyingController): def on_submit(self): self.update_requested_qty_in_production_plan() self.update_requested_qty() - if self.material_request_type == "Purchase": + if self.material_request_type == "Purchase" and frappe.db.exists( + "Budget", {"applicable_on_material_request": 1, "docstatus": 1} + ): self.validate_budget() def before_save(self): From d65be69c4c6199d72847999c5cd940c5893aa2a2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 19:13:08 +0530 Subject: [PATCH 34/35] fix: incoming rate for sales return with Moving Average valuation method (backport #38849) (backport #38863) (#38866) fix: incoming rate for sales return with Moving Average valuation method (backport #38849) (#38863) * fix: incoming rate for sales return with Moving Average valuation method (#38849) (cherry picked from commit 7fdac62393ee1e96969cca38a4ce0c07993dce7e) # Conflicts: # erpnext/stock/doctype/delivery_note/test_delivery_note.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure (cherry picked from commit 4057682c878bd6abffa63e7343ba33bd220bc88c) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- erpnext/controllers/selling_controller.py | 8 +-- .../delivery_note/test_delivery_note.py | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e8bae8cda5..4489d60131 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor -from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.utils import get_incoming_rate, get_valuation_method class SellingController(StockController): @@ -432,11 +432,13 @@ class SellingController(StockController): items = self.get("items") + (self.get("packed_items") or []) for d in items: - if not self.get("return_against"): + if not self.get("return_against") or ( + get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + ): # 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 (self.get("is_return") and d.incoming_rate): + if not d.incoming_rate: 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 3a581226ca..da8ee022f9 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1375,6 +1375,56 @@ class TestDeliveryNote(FrappeTestCase): dn.reload() self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered") + def test_sales_return_valuation_for_moving_average(self): + item_code = make_item( + "_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"} + ).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, qty=5, rate=500, posting_date=add_days(nowdate(), -4) + ) + self.assertEqual(dn.items[0].incoming_rate, 100.0) + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=200.0, + posting_date=add_days(nowdate(), -3), + ) + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=300.0, + posting_date=add_days(nowdate(), -2), + ) + + dn1 = create_delivery_note( + is_return=1, + item_code=item_code, + return_against=dn.name, + qty=-5, + rate=500, + company=dn.company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + do_not_submit=1, + posting_date=add_days(nowdate(), -1), + ) + + # (300 * 5) + (200 * 5) = 2500 + # 2500 / 10 = 250 + + self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From 4e27174c851d2283c2d6c01767ac95b05c0b46cd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:12:20 +0530 Subject: [PATCH 35/35] fix: set `fg-itm-qty` based on `qty` instead of the other way round in Subcontracting POs (backport #38842) (#38855) fix: set `fg-itm-qty` based on `qty` instead of the other way round (cherry picked from commit a99d0a65b028b05b5c3bcb46a48a16771d00861f) Co-authored-by: Gughan Ravikumar --- erpnext/buying/doctype/purchase_order/purchase_order.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 88faeee982..3b671bb239 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -214,7 +214,7 @@ frappe.ui.form.on("Purchase Order Item", { } }, - fg_item_qty: async function(frm, cdt, cdn) { + qty: async function (frm, cdt, cdn) { if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) { var row = locals[cdt][cdn]; @@ -222,7 +222,7 @@ frappe.ui.form.on("Purchase Order Item", { var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item) if (result.message && row.item_code == result.message.service_item && row.uom == result.message.service_item_uom) { - frappe.model.set_value(cdt, cdn, "qty", flt(row.fg_item_qty) * flt(result.message.conversion_factor)); + frappe.model.set_value(cdt, cdn, "fg_item_qty", flt(row.qty) / flt(result.message.conversion_factor)); } } }