diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 1a4747c55b8..7f5b52e4615 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -148,6 +148,9 @@ def start_import( import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records") data = parse_data_from_template(import_file.raw_data) + # Importer expects 'Data Import' class, which has 'payload_count' attribute + if not data_import.get("payload_count"): + data_import.payload_count = len(data) - 1 if import_file_path: add_bank_account(data, bank_account) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 5e17881b6c1..4246ba5c032 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -56,17 +56,17 @@ class BankTransaction(Document): Bank Transaction should be on the same currency as the Bank Account. """ if self.currency and self.bank_account: - account = frappe.get_cached_value("Bank Account", self.bank_account, "account") - account_currency = frappe.get_cached_value("Account", account, "account_currency") + if account := frappe.get_cached_value("Bank Account", self.bank_account, "account"): + account_currency = frappe.get_cached_value("Account", account, "account_currency") - if self.currency != account_currency: - frappe.throw( - _( - "Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}" - ).format( - frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency) + if self.currency != account_currency: + frappe.throw( + _( + "Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}" + ).format( + frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency) + ) ) - ) def set_status(self): if self.docstatus == 2: diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js index d931f627dbd..ad68352c2a4 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.js @@ -3,22 +3,36 @@ frappe.ui.form.on("Currency Exchange Settings", { service_provider: function (frm) { - if (frm.doc.service_provider == "exchangerate.host") { - let result = ["result"]; - let params = { - date: "{transaction_date}", - from: "{from_currency}", - to: "{to_currency}", - }; - add_param(frm, "https://api.exchangerate.host/convert", params, result); - } else if (frm.doc.service_provider == "frankfurter.app") { - let result = ["rates", "{to_currency}"]; - let params = { - base: "{from_currency}", - symbols: "{to_currency}", - }; - add_param(frm, "https://frankfurter.app/{transaction_date}", params, result); - } + frm.call({ + method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint", + args: { + service_provider: frm.doc.service_provider, + use_http: frm.doc.use_http, + }, + callback: function (r) { + if (r && r.message) { + if (frm.doc.service_provider == "exchangerate.host") { + let result = ["result"]; + let params = { + date: "{transaction_date}", + from: "{from_currency}", + to: "{to_currency}", + }; + add_param(frm, r.message, params, result); + } else if (frm.doc.service_provider == "frankfurter.app") { + let result = ["rates", "{to_currency}"]; + let params = { + base: "{from_currency}", + symbols: "{to_currency}", + }; + add_param(frm, r.message, params, result); + } + } + }, + }); + }, + use_http: function (frm) { + frm.trigger("service_provider"); }, }); diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index df232a5848c..bd90b8add80 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -9,6 +9,7 @@ "disabled", "service_provider", "api_endpoint", + "use_http", "access_key", "url", "column_break_3", @@ -91,12 +92,19 @@ "fieldname": "access_key", "fieldtype": "Data", "label": "Access Key" + }, + { + "default": "0", + "depends_on": "eval: doc.service_provider != \"Custom\"", + "fieldname": "use_http", + "fieldtype": "Check", + "label": "Use HTTP Protocol" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-10-04 15:30:25.333860", + "modified": "2024-03-18 08:32:26.895076", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index 3393d4170bc..b8817c60572 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -31,6 +31,7 @@ class CurrencyExchangeSettings(Document): result_key: DF.Table[CurrencyExchangeSettingsResult] service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"] url: DF.Data | None + use_http: DF.Check # end: auto-generated types def validate(self): @@ -53,7 +54,7 @@ class CurrencyExchangeSettings(Document): self.set("result_key", []) self.set("req_params", []) - self.api_endpoint = "https://api.exchangerate.host/convert" + self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http) self.append("result_key", {"key": "result"}) self.append("req_params", {"key": "access_key", "value": self.access_key}) self.append("req_params", {"key": "amount", "value": "1"}) @@ -64,7 +65,7 @@ class CurrencyExchangeSettings(Document): self.set("result_key", []) self.set("req_params", []) - self.api_endpoint = "https://frankfurter.app/{transaction_date}" + self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http) self.append("result_key", {"key": "rates"}) self.append("result_key", {"key": "{to_currency}"}) self.append("req_params", {"key": "base", "value": "{from_currency}"}) @@ -103,3 +104,19 @@ class CurrencyExchangeSettings(Document): frappe.throw(_("Returned exchange rate is neither integer not float.")) self.url = response.url + + +@frappe.whitelist() +def get_api_endpoint(service_provider: str = None, use_http: bool = False): + if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]: + if service_provider == "exchangerate.host": + api = "api.exchangerate.host/convert" + elif service_provider == "frankfurter.app": + api = "frankfurter.app/{transaction_date}" + + protocol = "https://" + if use_http: + protocol = "http://" + + return protocol + api + return None diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index e9cbb337d5b..3797828081a 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -628,21 +628,21 @@ def get_account_details( if account_balance and ( account_balance[0].balance or account_balance[0].balance_in_account_currency ): - account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance( + if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance( company, posting_date, account_balance - ) - row = account_with_new_balance[0] - account_details.update( - { - "balance_in_base_currency": row["balance_in_base_currency"], - "balance_in_account_currency": row["balance_in_account_currency"], - "current_exchange_rate": row["current_exchange_rate"], - "new_exchange_rate": row["new_exchange_rate"], - "new_balance_in_base_currency": row["new_balance_in_base_currency"], - "new_balance_in_account_currency": row["new_balance_in_account_currency"], - "zero_balance": row["zero_balance"], - "gain_loss": row["gain_loss"], - } - ) + ): + row = account_with_new_balance[0] + account_details.update( + { + "balance_in_base_currency": row["balance_in_base_currency"], + "balance_in_account_currency": row["balance_in_account_currency"], + "current_exchange_rate": row["current_exchange_rate"], + "new_exchange_rate": row["new_exchange_rate"], + "new_balance_in_base_currency": row["new_balance_in_base_currency"], + "new_balance_in_account_currency": row["new_balance_in_account_currency"], + "zero_balance": row["zero_balance"], + "gain_loss": row["gain_loss"], + } + ) return account_details diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index f6d35fe2bba..3186d07adcc 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -196,7 +196,7 @@ frappe.ui.form.on("Journal Entry", { !(frm.doc.accounts || []).length || ((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account) ) { - if (in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) { + if (["Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) { return frappe.call({ type: "GET", method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account", @@ -308,7 +308,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro filters: [[jvd.reference_type, "docstatus", "=", 1]], }; - if (in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) { + if (["Sales Invoice", "Purchase Invoice"].includes(jvd.reference_type)) { out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]); // Filter by cost center if (jvd.cost_center) { @@ -320,7 +320,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]); } - if (in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) { + if (["Sales Order", "Purchase Order"].includes(jvd.reference_type)) { // party_type and party mandatory frappe.model.validate_missing(jvd, "party_type"); frappe.model.validate_missing(jvd, "party"); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ab50c38b1e2..0cb1a3d4997 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -32,7 +32,7 @@ frappe.ui.form.on("Payment Entry", { frm.set_query("paid_from", function () { frm.events.validate_company(frm); - var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) + var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; return { @@ -87,7 +87,7 @@ frappe.ui.form.on("Payment Entry", { frm.set_query("paid_to", function () { frm.events.validate_company(frm); - var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) + var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; return { @@ -134,7 +134,7 @@ frappe.ui.form.on("Payment Entry", { frm.set_query("payment_term", "references", function (frm, cdt, cdn) { const child = locals[cdt][cdn]; if ( - in_list(["Purchase Invoice", "Sales Invoice"], child.reference_doctype) && + ["Purchase Invoice", "Sales Invoice"].includes(child.reference_doctype) && child.reference_name ) { return { @@ -395,10 +395,6 @@ frappe.ui.form.on("Payment Entry", { return { query: "erpnext.controllers.queries.employee_query", }; - } else if (frm.doc.party_type == "Customer") { - return { - query: "erpnext.controllers.queries.customer_query", - }; } }); @@ -627,7 +623,7 @@ frappe.ui.form.on("Payment Entry", { if (frm.doc.paid_from_account_currency == company_currency) { frm.set_value("source_exchange_rate", 1); } else if (frm.doc.paid_from) { - if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) { + if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) { let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", @@ -1046,7 +1042,7 @@ frappe.ui.form.on("Payment Entry", { } var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding; - } else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) { + } else if (["Customer", "Supplier"].includes(frm.doc.party_type)) { total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount")); if (paid_amount > total_negative_outstanding) { if (total_negative_outstanding == 0) { @@ -1217,7 +1213,7 @@ frappe.ui.form.on("Payment Entry", { if ( frm.doc.party_type == "Customer" && - !in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype) + !["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"].includes(row.reference_doctype) ) { frappe.model.set_value(row.doctype, row.name, "reference_doctype", null); frappe.msgprint( @@ -1231,7 +1227,7 @@ frappe.ui.form.on("Payment Entry", { if ( frm.doc.party_type == "Supplier" && - !in_list(["Purchase Order", "Purchase Invoice", "Journal Entry"], row.reference_doctype) + !["Purchase Order", "Purchase Invoice", "Journal Entry"].includes(row.reference_doctype) ) { frappe.model.set_value(row.doctype, row.name, "against_voucher_type", null); frappe.msgprint( @@ -1327,7 +1323,7 @@ frappe.ui.form.on("Payment Entry", { bank_account: function (frm) { const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to"; - if (frm.doc.bank_account && in_list(["Pay", "Receive"], frm.doc.payment_type)) { + if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) { frappe.call({ method: "erpnext.accounts.doctype.bank_account.bank_account.get_bank_account_details", args: { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c031be53d65..29603f7458b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -404,7 +404,9 @@ class PaymentEntry(AccountsController): ref_details = get_reference_details( d.reference_doctype, d.reference_name, self.party_account_currency ) - if ref_exchange_rate: + + # Only update exchange rate when the reference is Journal Entry + if ref_exchange_rate and d.reference_doctype == "Journal Entry": ref_details.update({"exchange_rate": ref_exchange_rate}) for field, value in ref_details.items(): @@ -526,9 +528,9 @@ class PaymentEntry(AccountsController): def get_valid_reference_doctypes(self): if self.party_type == "Customer": - return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") + return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry") elif self.party_type == "Supplier": - return ("Purchase Order", "Purchase Invoice", "Journal Entry") + return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry") elif self.party_type == "Shareholder": return ("Journal Entry",) elif self.party_type == "Employee": @@ -1191,6 +1193,7 @@ class PaymentEntry(AccountsController): "Journal Entry", "Sales Order", "Purchase Order", + "Payment Entry", ): self.add_advance_gl_for_reference(gl_entries, ref) @@ -1213,7 +1216,9 @@ class PaymentEntry(AccountsController): if getdate(posting_date) < getdate(self.posting_date): posting_date = self.posting_date - dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Sales Order"] else "debit" + dr_or_cr = ( + "credit" if invoice.reference_doctype in ["Sales Invoice", "Payment Entry"] else "debit" + ) args_dict["account"] = invoice.account args_dict[dr_or_cr] = invoice.allocated_amount args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount @@ -1660,7 +1665,7 @@ def get_outstanding_reference_documents(args, validate=False): outstanding_invoices = get_outstanding_invoices( args.get("party_type"), args.get("party"), - party_account, + [party_account], common_filter=common_filter, posting_date=posting_and_due_date, min_outstanding=args.get("outstanding_amt_greater_than"), diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 04066666c9e..2b226e1b241 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1514,6 +1514,168 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(references[1].payment_term, "Basic Amount Receivable") self.assertEqual(references[2].payment_term, "Tax Receivable") + def test_reverse_payment_reconciliation(self): + customer = create_customer(frappe.generate_hash(length=10), "INR") + pe = create_payment_entry( + party_type="Customer", + party=customer, + payment_type="Receive", + paid_from="Debtors - _TC", + paid_to="_Test Cash - _TC", + ) + pe.submit() + + reverse_pe = create_payment_entry( + party_type="Customer", + party=customer, + payment_type="Pay", + paid_from="_Test Cash - _TC", + paid_to="Debtors - _TC", + ) + reverse_pe.submit() + + pr = frappe.get_doc("Payment Reconciliation") + pr.company = "_Test Company" + pr.party_type = "Customer" + pr.party = customer + pr.receivable_payable_account = "Debtors - _TC" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + self.assertEqual(reverse_pe.name, pr.invoices[0].invoice_number) + self.assertEqual(pe.name, pr.payments[0].reference_name) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + def test_advance_reverse_payment_reconciliation(self): + from erpnext.accounts.doctype.account.test_account import create_account + + company = "_Test Company" + customer = create_customer(frappe.generate_hash(length=10), "INR") + advance_account = create_account( + parent_account="Current Assets - _TC", + account_name="Advances Received", + company=company, + account_type="Receivable", + ) + + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": 1, + "default_advance_received_account": advance_account, + }, + ) + # Reverse Payment(essentially an Invoice) + reverse_pe = create_payment_entry( + party_type="Customer", + party=customer, + payment_type="Pay", + paid_from="_Test Cash - _TC", + paid_to=advance_account, + ) + reverse_pe.save() # use save() to trigger set_liability_account() + reverse_pe.submit() + + # Advance Payment + pe = create_payment_entry( + party_type="Customer", + party=customer, + payment_type="Receive", + paid_from=advance_account, + paid_to="_Test Cash - _TC", + ) + pe.save() # use save() to trigger set_liability_account() + pe.submit() + + # Partially reconcile advance against invoice + pr = frappe.get_doc("Payment Reconciliation") + pr.company = company + pr.party_type = "Customer" + pr.party = customer + pr.receivable_payable_account = "Debtors - _TC" + pr.default_advance_account = advance_account + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].allocated_amount = 400 + pr.reconcile() + + # assert General and Payment Ledger entries post partial reconciliation + self.expected_gle = [ + {"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0}, + {"account": advance_account, "debit": 400.0, "credit": 0.0}, + {"account": advance_account, "debit": 0.0, "credit": 1000.0}, + {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0}, + ] + self.expected_ple = [ + { + "account": advance_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": -1000.0, + }, + { + "account": "Debtors - _TC", + "voucher_no": pe.name, + "against_voucher_no": reverse_pe.name, + "amount": -400.0, + }, + { + "account": advance_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": 400.0, + }, + ] + self.voucher_no = pe.name + self.check_gl_entries() + self.check_pl_entries() + + # Unreconcile + unrecon = ( + frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + "allocations": [{"reference_doctype": reverse_pe.doctype, "reference_name": reverse_pe.name}], + } + ) + .save() + .submit() + ) + + # assert General and Payment Ledger entries post unreconciliation + self.expected_gle = [ + {"account": advance_account, "debit": 0.0, "credit": 1000.0}, + {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0}, + ] + self.expected_ple = [ + { + "account": advance_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": -1000.0, + }, + ] + self.voucher_no = pe.name + self.check_gl_entries() + self.check_pl_entries() + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 1bf1acee70d..2c4952a0c66 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -340,10 +340,15 @@ class PaymentReconciliation(Document): self.build_qb_filter_conditions(get_invoices=True) + accounts = [self.receivable_payable_account] + + if self.default_advance_account: + accounts.append(self.default_advance_account) + non_reconciled_invoices = get_outstanding_invoices( self.party_type, self.party, - self.receivable_payable_account, + accounts, common_filter=self.common_filter_conditions, posting_date=self.ple_posting_date_filter, min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 301e6ef625c..1d20a5b954d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1130,6 +1130,17 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.allocation[0].allocated_amount, 85) self.assertEqual(pr.allocation[0].difference_amount, 0) + pr.reconcile() + si.reload() + self.assertEqual(si.outstanding_amount, 0) + # No Exchange Gain/Loss journal should be generated + exc_gain_loss_journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": si.doctype, "reference_name": si.name, "docstatus": 1}, + fields=["parent"], + ) + self.assertEqual(exc_gain_loss_journals, []) + def test_reconciliation_purchase_invoice_against_return(self): self.supplier = "_Test Supplier USD" pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index e5a6040c735..e45aa512fe8 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -28,7 +28,7 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) { if ( frm.doc.payment_request_type == "Inward" && frm.doc.payment_channel !== "Phone" && - !in_list(["Initiated", "Paid"], frm.doc.status) && + !["Initiated", "Paid"].includes(frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus == 1 ) { diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 5307ccb1931..81ebf9744c4 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -89,10 +89,11 @@ - - - - + + + + + @@ -101,6 +102,7 @@ +
30 Days60 Days90 Days120 Days0 - 30 Days30 - 60 Days60 - 90 Days90 - 120 DaysAbove 120 Days
{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }} {{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }} {{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}{{ frappe.utils.fmt_money(ageing.range5, currency=filters.presentation_currency) }}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index d6455b2002a..957611f7858 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -3,6 +3,8 @@ frappe.provide("erpnext.accounts"); +cur_frm.cscript.tax_table = "Purchase Taxes and Charges"; + erpnext.accounts.payment_triggers.setup("Purchase Invoice"); erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges"); erpnext.accounts.taxes.setup_tax_validations("Purchase Invoice"); diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 3ee4214ae71..66df76a3af0 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -745,6 +745,7 @@ "fieldtype": "Currency", "label": "Landed Cost Voucher Amount", "no_copy": 1, + "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 }, @@ -938,7 +939,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-02-04 14:11:52.742228", + "modified": "2024-03-19 19:09:47.210965", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js index 66a9cbe8440..4c94503c184 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.js @@ -1,6 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +cur_frm.cscript.tax_table = "Purchase Taxes and Charges"; erpnext.accounts.taxes.setup_tax_validations("Purchase Taxes and Charges Template"); erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges"); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 17101cd2720..c7505ce007d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -3,6 +3,8 @@ frappe.provide("erpnext.accounts"); +cur_frm.cscript.tax_table = "Sales Taxes and Charges"; + erpnext.accounts.taxes.setup_tax_validations("Sales Invoice"); erpnext.accounts.payment_triggers.setup("Sales Invoice"); erpnext.accounts.pos.setup("Sales Invoice"); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 8fd897bddf1..4f6e5ba9206 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2170,7 +2170,8 @@ "description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.", "fieldname": "update_outstanding_for_self", "fieldtype": "Check", - "label": "Update Outstanding for Self" + "label": "Update Outstanding for Self", + "no_copy": 1 } ], "icon": "fa fa-file-text", @@ -2183,7 +2184,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-03-11 14:20:34.874192", + "modified": "2024-03-15 16:44:17.778370", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6a01ccf3409..5b031e82387 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1571,6 +1571,12 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000) self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500) + def test_zero_qty_return_invoice_with_stock_effect(self): + cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True) + cr_note.update_stock = True + cr_note.items[0].qty = 0 + self.assertRaises(frappe.ValidationError, cr_note.save) + def test_return_invoice_with_account_mismatch(self): debtors2 = create_account( parent_account="Accounts Receivable - _TC", @@ -3932,7 +3938,6 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter ) supplier.append("companies", {"company": allowed_to_interact_with}) - supplier.insert() supplier_name = supplier.name else: diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js index 91d4d047f8f..c42623addb5 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js @@ -1,5 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +cur_frm.cscript.tax_table = "Sales Taxes and Charges"; erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template"); erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges"); diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 558eeaa6d35..0e6c041d24d 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1015,7 +1015,7 @@ def get_outstanding_invoices( if account: root_type, account_type = frappe.get_cached_value( - "Account", account, ["root_type", "account_type"] + "Account", account[0], ["root_type", "account_type"] ) party_account_type = "Receivable" if root_type == "Asset" else "Payable" party_account_type = account_type or party_account_type @@ -1026,7 +1026,7 @@ def get_outstanding_invoices( common_filter = common_filter or [] common_filter.append(ple.account_type == party_account_type) - common_filter.append(ple.account == account) + common_filter.append(ple.account.isin(account)) common_filter.append(ple.party_type == party_type) common_filter.append(ple.party == party) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 6dbb53a078f..0f71e5d6f60 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -79,7 +79,7 @@ frappe.ui.form.on("Asset", { frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); if (frm.doc.docstatus == 1) { - if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { + if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) { frm.add_custom_button( __("Transfer Asset"), function () { @@ -365,7 +365,7 @@ frappe.ui.form.on("Asset", { if (v.journal_entry) { asset_values.push(asset_value); } else { - if (in_list(["Scrapped", "Sold"], frm.doc.status)) { + if (["Scrapped", "Sold"].includes(frm.doc.status)) { asset_values.push(null); } else { asset_values.push(asset_value); @@ -400,7 +400,7 @@ frappe.ui.form.on("Asset", { }); } - if (in_list(["Scrapped", "Sold"], frm.doc.status)) { + if (["Scrapped", "Sold"].includes(frm.doc.status)) { x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: "Date" })); asset_values.push(0); } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 7875646ab72..cf383021b06 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -4,6 +4,8 @@ frappe.provide("erpnext.buying"); frappe.provide("erpnext.accounts.dimensions"); +cur_frm.cscript.tax_table = "Purchase Taxes and Charges"; + erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges"); erpnext.accounts.taxes.setup_tax_validations("Purchase Order"); erpnext.buying.setup_buying_controller(); @@ -289,7 +291,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( this.frm.fields_dict.items_section.wrapper.removeClass("hide-border"); } - if (!in_list(["Closed", "Delivered"], doc.status)) { + if (!["Closed", "Delivered"].includes(doc.status)) { if ( this.frm.doc.status !== "Closed" && flt(this.frm.doc.per_received, 2) < 100 && @@ -334,7 +336,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( this.frm.page.set_inner_btn_group_as_primary(__("Status")); } - } else if (in_list(["Closed", "Delivered"], doc.status)) { + } else if (["Closed", "Delivered"].includes(doc.status)) { if (this.frm.has_perm("submit")) { this.frm.add_custom_button( __("Re-open"), @@ -507,7 +509,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( target: me.frm, setters: { schedule_date: undefined, - status: undefined, }, get_query_filters: { material_request_type: "Purchase", diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 60dd54c2385..3dae0442cce 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -485,7 +485,7 @@ "link_fieldname": "party" } ], - "modified": "2023-10-19 16:55:15.148325", + "modified": "2024-03-13 11:14:06.516519", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", @@ -544,7 +544,7 @@ } ], "quick_entry": 1, - "search_fields": "supplier_name, supplier_group", + "search_fields": "supplier_group", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC", diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 350a25f26e7..55974ea6c46 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -154,44 +154,6 @@ class TestSupplier(FrappeTestCase): # Rollback address.delete() - def test_serach_fields_for_supplier(self): - from erpnext.controllers.queries import supplier_query - - frappe.db.set_single_value("Buying Settings", "supp_master_name", "Naming Series") - - supplier_name = create_supplier(supplier_name="Test Supplier 1").name - - make_property_setter( - "Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype" - ) - - data = supplier_query( - "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True - ) - - self.assertEqual(data[0].name, supplier_name) - self.assertEqual(data[0].supplier_group, "Services") - self.assertTrue("supplier_type" not in data[0]) - - make_property_setter( - "Supplier", - None, - "search_fields", - "supplier_group, supplier_type", - "Data", - for_doctype="Doctype", - ) - data = supplier_query( - "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True - ) - - self.assertEqual(data[0].name, supplier_name) - self.assertEqual(data[0].supplier_group, "Services") - self.assertEqual(data[0].supplier_type, "Company") - self.assertTrue("supplier_type" in data[0]) - - frappe.db.set_single_value("Buying Settings", "supp_master_name", "Supplier Name") - def create_supplier(**args): args = frappe._dict(args) diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js index c109abd8146..f7d0d947b61 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.js @@ -77,7 +77,10 @@ frappe.query_reports["Supplier Quotation Comparison"] = { fieldname: "group_by", label: __("Group by"), fieldtype: "Select", - options: [__("Group by Supplier"), __("Group by Item")], + options: [ + { label: __("Group by Supplier"), value: "Group by Supplier" }, + { label: __("Group by Item"), value: "Group by Item" }, + ], default: __("Group by Supplier"), }, { diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fe3c5d7a836..ddc3acf6556 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -89,6 +89,7 @@ force_item_fields = ( "weight_per_unit", "weight_uom", "total_weight", + "valuation_rate", ) @@ -168,6 +169,13 @@ class AccountsController(TransactionBase): if not self.get("is_return") and not self.get("is_debit_note"): self.validate_qty_is_not_zero() + if ( + self.doctype in ["Sales Invoice", "Purchase Invoice"] + and self.get("is_return") + and self.get("update_stock") + ): + self.validate_zero_qty_for_return_invoices_with_stock() + if self.get("_action") and self._action != "update_after_submit": self.set_missing_values(for_validate=True) @@ -602,23 +610,31 @@ class AccountsController(TransactionBase): ) def validate_due_date(self): - if self.get("is_pos"): + if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]: return from erpnext.accounts.party import validate_due_date - if self.doctype == "Sales Invoice": + posting_date = ( + self.posting_date if self.doctype == "Sales Invoice" else (self.bill_date or self.posting_date) + ) + + # skip due date validation for records via Data Import + if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date): + self.due_date = posting_date + + elif self.doctype == "Sales Invoice": if not self.due_date: frappe.throw(_("Due Date is mandatory")) validate_due_date( - self.posting_date, + posting_date, self.due_date, self.payment_terms_template, ) elif self.doctype == "Purchase Invoice": validate_due_date( - self.bill_date or self.posting_date, + posting_date, self.due_date, self.bill_date, self.payment_terms_template, @@ -1044,6 +1060,18 @@ class AccountsController(TransactionBase): else: return flt(args.get(field, 0) / self.get("conversion_rate", 1)) + def validate_zero_qty_for_return_invoices_with_stock(self): + rows = [] + for item in self.items: + if not flt(item.qty): + rows.append(item) + if rows: + frappe.throw( + _( + "For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}" + ).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows]))) + ) + def validate_qty_is_not_zero(self): if self.doctype == "Purchase Receipt": return @@ -2668,14 +2696,20 @@ def get_advance_journal_entries( else: q = q.where(journal_acc.debit_in_account_currency > 0) + reference_or_condition = [] + if include_unallocated: - q = q.where((journal_acc.reference_name.isnull()) | (journal_acc.reference_name == "")) + reference_or_condition.append(journal_acc.reference_name.isnull()) + reference_or_condition.append(journal_acc.reference_name == "") if order_list: - q = q.where( + reference_or_condition.append( (journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list)) ) + if reference_or_condition: + q = q.where(Criterion.any(reference_or_condition)) + q = q.orderby(journal_entry.posting_date) journal_entries = q.run(as_dict=True) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 821185766eb..c5307270156 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -513,6 +513,14 @@ class BuyingController(SubcontractingController): (not cint(self.is_return) and self.docstatus == 1) or (cint(self.is_return) and self.docstatus == 2) ): + serial_and_batch_bundle = d.get("serial_and_batch_bundle") + if self.is_internal_transfer() and self.is_return and self.docstatus == 2: + serial_and_batch_bundle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": d.name, "warehouse": d.from_warehouse}, + "serial_and_batch_bundle", + ) + from_warehouse_sle = self.get_sl_entries( d, { @@ -521,19 +529,24 @@ class BuyingController(SubcontractingController): "outgoing_rate": d.rate, "recalculate_rate": 1, "dependant_sle_voucher_detail_no": d.name, + "serial_and_batch_bundle": serial_and_batch_bundle, }, ) sl_entries.append(from_warehouse_sle) + type_of_transaction = "Inward" + if self.docstatus == 2: + type_of_transaction = "Outward" + sle = self.get_sl_entries( d, { "actual_qty": flt(pr_qty), "serial_and_batch_bundle": ( d.serial_and_batch_bundle - if not self.is_internal_transfer() - else self.get_package_for_target_warehouse(d) + if not self.is_internal_transfer() or self.is_return + else self.get_package_for_target_warehouse(d, type_of_transaction=type_of_transaction) ), }, ) @@ -570,7 +583,17 @@ class BuyingController(SubcontractingController): or (cint(self.is_return) and self.docstatus == 1) ): from_warehouse_sle = self.get_sl_entries( - d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1} + d, + { + "actual_qty": -1 * pr_qty, + "warehouse": d.from_warehouse, + "recalculate_rate": 1, + "serial_and_batch_bundle": ( + self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward") + if self.is_internal_transfer() and self.is_return + else None + ), + }, ) sl_entries.append(from_warehouse_sle) @@ -597,13 +620,15 @@ class BuyingController(SubcontractingController): via_landed_cost_voucher=via_landed_cost_voucher, ) - def get_package_for_target_warehouse(self, item) -> str: + def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str: if not item.serial_and_batch_bundle: return "" + if not warehouse: + warehouse = item.warehouse + return self.make_package_for_transfer( - item.serial_and_batch_bundle, - item.warehouse, + item.serial_and_batch_bundle, warehouse, type_of_transaction=type_of_transaction ) def update_ordered_and_reserved_qty(self): diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 6463d17cd90..a7b2d572733 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -85,79 +85,6 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, ) - # searches for customer - - -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): - doctype = "Customer" - conditions = [] - cust_master_name = frappe.defaults.get_user_default("cust_master_name") - - fields = ["name"] - if cust_master_name != "Customer Name": - fields.append("customer_name") - - fields = get_fields(doctype, fields) - searchfields = frappe.get_meta(doctype).get_search_fields() - searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) - - return frappe.db.sql( - """select {fields} from `tabCustomer` - where docstatus < 2 - and ({scond}) and disabled=0 - {fcond} {mcond} - order by - (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end), - (case when locate(%(_txt)s, customer_name) > 0 then locate(%(_txt)s, customer_name) else 99999 end), - idx desc, - name, customer_name - limit %(page_len)s offset %(start)s""".format( - **{ - "fields": ", ".join(fields), - "scond": searchfields, - "mcond": get_match_cond(doctype), - "fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"), - } - ), - {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, - as_dict=as_dict, - ) - - -# searches for supplier -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): - doctype = "Supplier" - supp_master_name = frappe.defaults.get_user_default("supp_master_name") - - fields = ["name"] - if supp_master_name != "Supplier Name": - fields.append("supplier_name") - - fields = get_fields(doctype, fields) - - return frappe.db.sql( - """select {field} from `tabSupplier` - where docstatus < 2 - and ({key} like %(txt)s - or supplier_name like %(txt)s) and disabled=0 - and (on_hold = 0 or (on_hold = 1 and CURRENT_DATE > release_date)) - {mcond} - order by - (case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end), - (case when locate(%(_txt)s, supplier_name) > 0 then locate(%(_txt)s, supplier_name) else 99999 end), - idx desc, - name, supplier_name - limit %(page_len)s offset %(start)s""".format( - **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} - ), - {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, - as_dict=as_dict, - ) - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index de431f3d42d..3ea3bcdfcc8 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -423,6 +423,15 @@ def make_return_doc( ]: type_of_transaction = "Outward" + warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse + if source_parent.doctype in [ + "Sales Invoice", + "POS Invoice", + "Delivery Note", + ] and source_parent.get("is_internal_customer"): + type_of_transaction = "Outward" + warehouse = source_doc.target_warehouse + cls_obj = SerialBatchCreation( { "type_of_transaction": type_of_transaction, @@ -432,7 +441,7 @@ def make_return_doc( "returned_serial_nos": returned_serial_nos, "voucher_type": source_parent.doctype, "do_not_submit": True, - "warehouse": source_doc.warehouse, + "warehouse": warehouse, "has_serial_no": item_details.has_serial_no, "has_batch_no": item_details.has_batch_no, } @@ -575,11 +584,14 @@ def make_return_doc( 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) and not target_doc.get("use_serial_batch_fields"): + if not target_doc.get("use_serial_batch_fields"): + for qty_field in ["stock_qty", "rejected_qty"]: + if not target_doc.get(qty_field): + continue + update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field) - elif target_doc.get(qty_field) and target_doc.get("use_serial_batch_fields"): - update_non_bundled_serial_nos(source_doc, target_doc, source_parent) + elif target_doc.get("use_serial_batch_fields"): + update_non_bundled_serial_nos(source_doc, target_doc, source_parent) def update_non_bundled_serial_nos(source_doc, target_doc, source_parent): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index ecc28a89fa5..747b4e061b6 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -442,8 +442,10 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty")) - if not d.incoming_rate or ( - get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + if ( + not d.incoming_rate + or self.is_internal_transfer() + or (get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")) ): d.incoming_rate = get_incoming_rate( { @@ -458,6 +460,8 @@ class SellingController(StockController): "voucher_no": self.name, "voucher_detail_no": d.name, "allow_zero_valuation": d.get("allow_zero_valuation"), + "batch_no": d.batch_no, + "serial_no": d.serial_no, }, raise_error_if_no_rate=False, ) @@ -530,13 +534,26 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def get_sle_for_source_warehouse(self, item_row): + serial_and_batch_bundle = item_row.serial_and_batch_bundle + if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return: + if self.docstatus == 1: + serial_and_batch_bundle = self.make_package_for_transfer( + serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward" + ) + else: + serial_and_batch_bundle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse}, + "serial_and_batch_bundle", + ) + sle = self.get_sl_entries( item_row, { "actual_qty": -1 * flt(item_row.qty), "incoming_rate": item_row.incoming_rate, "recalculate_rate": cint(self.is_return), - "serial_and_batch_bundle": item_row.serial_and_batch_bundle, + "serial_and_batch_bundle": serial_and_batch_bundle, }, ) if item_row.target_warehouse and not cint(self.is_return): @@ -557,9 +574,15 @@ class SellingController(StockController): if item_row.warehouse: sle.dependant_sle_voucher_detail_no = item_row.name - if item_row.serial_and_batch_bundle: + if item_row.serial_and_batch_bundle and not cint(self.is_return): + type_of_transaction = "Inward" + if cint(self.is_return): + type_of_transaction = "Outward" + sle["serial_and_batch_bundle"] = self.make_package_for_transfer( - item_row.serial_and_batch_bundle, item_row.target_warehouse + item_row.serial_and_batch_bundle, + item_row.target_warehouse, + type_of_transaction=type_of_transaction, ) return sle diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 688600774cc..2b607eafdf8 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -236,6 +236,14 @@ class StockController(AccountsController): qty = row.get("rejected_qty") warehouse = row.get("rejected_warehouse") + if ( + self.is_internal_transfer() + and self.doctype in ["Sales Invoice", "Delivery Note"] + and self.is_return + ): + warehouse = row.get("target_warehouse") or row.get("warehouse") + type_of_transaction = "Outward" + bundle_details.update( { "qty": qty, @@ -579,7 +587,7 @@ class StockController(AccountsController): bundle_doc.warehouse = warehouse bundle_doc.type_of_transaction = type_of_transaction bundle_doc.voucher_type = self.doctype - bundle_doc.voucher_no = self.name + bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name bundle_doc.is_cancelled = 0 for row in bundle_doc.entries: @@ -595,6 +603,7 @@ class StockController(AccountsController): bundle_doc.calculate_qty_and_amount() bundle_doc.flags.ignore_permissions = True + bundle_doc.flags.ignore_validate = True bundle_doc.save(ignore_permissions=True) return bundle_doc.name diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 3a3bc1cd725..c536d1cb3da 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -31,18 +31,6 @@ class TestQueries(unittest.TestCase): self.assertGreaterEqual(len(query(txt="_Test Lead")), 4) self.assertEqual(len(query(txt="_Test Lead 4")), 1) - def test_customer_query(self): - query = add_default_params(queries.customer_query, "Customer") - - self.assertGreaterEqual(len(query(txt="_Test Customer")), 7) - self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1) - - def test_supplier_query(self): - query = add_default_params(queries.supplier_query, "Supplier") - - self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7) - self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1) - def test_item_query(self): query = add_default_params(queries.item_query, "Item") diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 0b6cdf25aac..609eab7f9a2 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -17,10 +17,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller } onload() { - this.frm.set_query("customer", function (doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.customer_query" }; - }); - this.frm.set_query("lead_owner", function (doc, cdt, cdn) { return { query: "frappe.core.doctype.user.user.user_query" }; }); diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 911907c4229..369704812e4 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -288,9 +288,6 @@ has_website_permission = { before_tests = "erpnext.setup.utils.before_tests" -standard_queries = { - "Customer": "erpnext.controllers.queries.customer_query", -} period_closing_doctypes = [ "Sales Invoice", diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 2ac28ea885f..6267ee4d029 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -400,7 +400,7 @@ frappe.ui.form.on("BOM", { }, rm_cost_as_per(frm) { - if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { + if (["Valuation Rate", "Last Purchase Rate"].includes(frm.doc.rm_cost_as_per)) { frm.set_value("plc_conversion_rate", 1.0); } }, diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 54d1414c814..6db901c71a4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -129,7 +129,7 @@ frappe.ui.form.on("Production Plan", { if ( frm.doc.mr_items && frm.doc.mr_items.length && - !in_list(["Material Requested", "Closed"], frm.doc.status) + !["Material Requested", "Closed"].includes(frm.doc.status) ) { frm.add_custom_button( __("Material Request"), diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 42f69438aef..1da33f0ad9b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -9,6 +9,8 @@ frappe.ui.form.on("Work Order", { "Job Card": "Create Job Card", }; + frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"]; + // Set query for warehouses frm.set_query("wip_warehouse", function () { return { @@ -194,7 +196,7 @@ frappe.ui.form.on("Work Order", { }, add_custom_button_to_return_components: function (frm) { - if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) { + if (frm.doc.docstatus === 1 && ["Closed", "Completed"].includes(frm.doc.status)) { let non_consumed_items = frm.doc.required_items.filter((d) => { return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty); }); @@ -594,7 +596,7 @@ erpnext.work_order = { ); } - if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) { + if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) { if (doc.status != "Stopped" && doc.status != "Completed") { frm.add_custom_button( __("Stop"), diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 16ac8db024b..49e8d8486a5 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -27,8 +27,6 @@ frappe.ui.form.on("Project", { }; }; - frm.set_query("customer", "erpnext.controllers.queries.customer_query"); - frm.set_query("user", "users", function () { return { query: "erpnext.projects.doctype.project.project.get_users_for_project", diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index d9187f8b678..c8905e14af2 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -20,7 +20,7 @@ frappe.ui.form.on("Communication", { ); } - if (!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { + if (!["Lead", "Opportunity"].includes(frm.doc.reference_doctype)) { frm.add_custom_button( __("Lead"), () => { diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 7879173cd11..c39fb524264 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -11,7 +11,7 @@ erpnext.accounts.taxes = { setup: function(frm) { // set conditional display for rate column in taxes $(frm.wrapper).on('grid-row-render', function(e, grid_row) { - if(in_list(['Sales Taxes and Charges', 'Purchase Taxes and Charges'], grid_row.doc.doctype)) { + if(['Sales Taxes and Charges', 'Purchase Taxes and Charges'].includes(grid_row.doc.doctype)) { me.set_conditional_mandatory_rate_or_amount(grid_row); } }); diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 1d0d47ec3d3..1e94c0032ab 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -74,11 +74,6 @@ erpnext.buying = { me.frm.set_query('billing_address', erpnext.queries.company_address_query); erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); - if(this.frm.fields_dict.supplier) { - this.frm.set_query("supplier", function() { - return{ query: "erpnext.controllers.queries.supplier_query" }}); - } - this.frm.set_query("item_code", "items", function() { if (me.frm.doc.is_subcontracted) { var filters = {'supplier': me.frm.doc.supplier}; @@ -134,7 +129,7 @@ erpnext.buying = { } toggle_subcontracting_fields() { - if (in_list(['Purchase Receipt', 'Purchase Invoice'], this.frm.doc.doctype)) { + if (['Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doc.doctype)) { this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM'); diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 527b762ace0..10fa7cebca8 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -9,7 +9,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { apply_pricing_rule_on_item(item) { let effective_item_rate = item.price_list_rate; let item_rate = item.rate; - if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) { + if (["Sales Order", "Quotation"].includes(item.parenttype) && item.blanket_order_rate) { effective_item_rate = item.blanket_order_rate; } if (item.margin_type == "Percentage") { @@ -26,7 +26,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100; } - if (item.discount_amount) { + if (item.discount_amount > 0) { item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item)); item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin); } @@ -52,7 +52,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Advance calculation applicable to Sales/Purchase Invoice if ( - in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype) + ["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype) && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return ) { @@ -60,7 +60,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } if ( - in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) + ["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype) && this.frm.doc.is_pos && this.frm.doc.is_return ) { @@ -69,7 +69,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } // Sales person's commission - if (in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) { + if (["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"].includes(this.frm.doc.doctype)) { this.calculate_commission(); this.calculate_contribution(); } @@ -562,7 +562,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment) : this.frm.doc.net_total); - if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) { + if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { this.frm.doc.base_grand_total = (this.frm.doc.total_taxes_and_charges) ? flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate) : this.frm.doc.base_net_total; } else { @@ -570,7 +570,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.frm.doc.taxes_and_charges_added = this.frm.doc.taxes_and_charges_deducted = 0.0; if(tax_count) { $.each(this.frm.doc["taxes"] || [], function(i, tax) { - if (in_list(["Valuation and Total", "Total"], tax.category)) { + if (["Valuation and Total", "Total"].includes(tax.category)) { if(tax.add_deduct_tax == "Add") { me.frm.doc.taxes_and_charges_added += flt(tax.tax_amount_after_discount_amount); } else { @@ -717,7 +717,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var actual_taxes_dict = {}; $.each(this.frm.doc["taxes"] || [], function(i, tax) { - if (in_list(["Actual", "On Item Quantity"], tax.charge_type)) { + if (["Actual", "On Item Quantity"].includes(tax.charge_type)) { var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount; tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; actual_taxes_dict[tax.idx] = tax_amount; @@ -762,7 +762,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // NOTE: // paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice // total_advance is only for non POS Invoice - if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_return){ + if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype) && this.frm.doc.is_return){ this.calculate_paid_amount(); } @@ -770,7 +770,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]); - if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) { + if(["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) { let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; @@ -793,7 +793,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.frm.refresh_field("base_paid_amount"); } - if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) { + if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { let total_amount_for_payment = (this.frm.doc.redeem_loyalty_points && this.frm.doc.loyalty_amount) ? flt(total_amount_to_pay - this.frm.doc.loyalty_amount, precision("base_grand_total")) : total_amount_to_pay; @@ -897,7 +897,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_change_amount(){ this.frm.doc.change_amount = 0.0; this.frm.doc.base_change_amount = 0.0; - if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) + if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype) && this.frm.doc.paid_amount > this.frm.doc.grand_total && !this.frm.doc.is_return) { var payment_types = $.map(this.frm.doc.payments, function(d) { return d.type; }); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 92cd737e465..9e3fac15305 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -315,7 +315,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } setup_quality_inspection() { - if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)) { + if(!["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype)) { return; } @@ -327,7 +327,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.page.set_inner_btn_group_as_primary(__('Create')); } - const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype) + const inspection_type = ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(this.frm.doc.doctype) ? "Incoming" : "Outgoing"; let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); @@ -359,7 +359,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe make_payment_request() { let me = this; - const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype)) + const payment_request_type = (['Sales Order', 'Sales Invoice'].includes(this.frm.doc.doctype)) ? "Inward" : "Outward"; frappe.call({ @@ -474,7 +474,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe setup_sms() { var me = this; let blacklist = ['Purchase Invoice', 'BOM']; - if(this.frm.doc.docstatus===1 && !in_list(["Lost", "Stopped", "Closed"], this.frm.doc.status) + if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status) && !blacklist.includes(this.frm.doctype)) { this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); }); } @@ -760,7 +760,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } on_submit() { - if (in_list(["Purchase Invoice", "Sales Invoice"], this.frm.doc.doctype) + if (["Purchase Invoice", "Sales Invoice"].includes(this.frm.doc.doctype) && !this.frm.doc.update_stock) { return; } @@ -864,7 +864,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } var set_party_account = function(set_pricing) { - if (in_list(["Sales Invoice", "Purchase Invoice"], me.frm.doc.doctype)) { + if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) { if(me.frm.doc.doctype=="Sales Invoice") { var party_type = "Customer"; var party_account_field = 'debit_to'; @@ -899,7 +899,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { + ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function() { set_party_account(set_pricing); }); @@ -1610,7 +1610,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "doctype": me.frm.doc.doctype, "name": me.frm.doc.name, "is_return": cint(me.frm.doc.is_return), - "update_stock": in_list(['Sales Invoice', 'Purchase Invoice'], me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0, + "update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0, "conversion_factor": me.frm.doc.conversion_factor, "pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '', "coupon_code": me.frm.doc.coupon_code @@ -2256,7 +2256,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe get_method_for_payment() { var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){ - if(in_list(['Sales Invoice', 'Purchase Invoice'], cur_frm.doc.doctype)){ + if(['Sales Invoice', 'Purchase Invoice'].includes( cur_frm.doc.doctype)){ method = "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_invoice"; }else { method= "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_order"; @@ -2496,7 +2496,7 @@ erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close } frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { - if (in_list(["Sales Invoice", "Delivery Note"], frm.doc.doctype)) { + if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) { item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward"; } else { item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward"; diff --git a/erpnext/public/js/payment/payments.js b/erpnext/public/js/payment/payments.js index 0e584205396..c91bb046a52 100644 --- a/erpnext/public/js/payment/payments.js +++ b/erpnext/public/js/payment/payments.js @@ -218,7 +218,7 @@ erpnext.payments = class payments extends erpnext.stock.StockController { update_paid_amount(update_write_off) { var me = this; - if (in_list(["change_amount", "write_off_amount"], this.idx)) { + if (["change_amount", "write_off_amount"].includes(this.idx)) { var value = me.selected_mode.val(); if (me.idx == "change_amount") { me.change_amount(value); diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index b7e685cd6fb..d7edac3cb9f 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -12,14 +12,6 @@ $.extend(erpnext.queries, { return { query: "erpnext.controllers.queries.lead_query" }; }, - customer: function () { - return { query: "erpnext.controllers.queries.customer_query" }; - }, - - supplier: function () { - return { query: "erpnext.controllers.queries.supplier_query" }; - }, - item: function (filters) { var args = { query: "erpnext.controllers.queries.item_query" }; if (filters) args["filters"] = filters; diff --git a/erpnext/public/js/sms_manager.js b/erpnext/public/js/sms_manager.js index d3147bb4600..63833da5af3 100644 --- a/erpnext/public/js/sms_manager.js +++ b/erpnext/public/js/sms_manager.js @@ -28,11 +28,11 @@ erpnext.SMSManager = function SMSManager(doc) { "Purchase Receipt": "Items has been received against purchase receipt: " + doc.name, }; - if (in_list(["Sales Order", "Delivery Note", "Sales Invoice"], doc.doctype)) + if (["Sales Order", "Delivery Note", "Sales Invoice"].includes(doc.doctype)) this.show(doc.contact_person, "Customer", doc.customer, "", default_msg[doc.doctype]); else if (doc.doctype === "Quotation") this.show(doc.contact_person, "Customer", doc.party_name, "", default_msg[doc.doctype]); - else if (in_list(["Purchase Order", "Purchase Receipt"], doc.doctype)) + else if (["Purchase Order", "Purchase Receipt"].includes(doc.doctype)) this.show(doc.contact_person, "Supplier", doc.supplier, "", default_msg[doc.doctype]); else if (doc.doctype == "Lead") this.show("", "", "", doc.mobile_no, default_msg[doc.doctype]); else if (doc.doctype == "Opportunity") diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 801376b2ed7..623941755d1 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -14,10 +14,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) { if (!args) { if ( (frm.doctype != "Purchase Order" && frm.doc.customer) || - (frm.doc.party_name && in_list(["Quotation", "Opportunity"], frm.doc.doctype)) + (frm.doc.party_name && ["Quotation", "Opportunity"].includes(frm.doc.doctype)) ) { let party_type = "Customer"; - if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) { + if (frm.doc.quotation_to && ["Lead", "Prospect"].includes(frm.doc.quotation_to)) { party_type = frm.doc.quotation_to; } diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index f2b7331cf3b..00df1c5c191 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -303,7 +303,7 @@ erpnext.sales_common = { if ((doc.packed_items || []).length) { $(this.frm.fields_dict.packing_list.row.wrapper).toggle(true); - if (in_list(["Delivery Note", "Sales Invoice"], doc.doctype)) { + if (["Delivery Note", "Sales Invoice"].includes(doc.doctype)) { var help_msg = "
" + __( @@ -315,7 +315,7 @@ erpnext.sales_common = { } } else { $(this.frm.fields_dict.packing_list.row.wrapper).toggle(false); - if (in_list(["Delivery Note", "Sales Invoice"], doc.doctype)) { + if (["Delivery Note", "Sales Invoice"].includes(doc.doctype)) { frappe.meta.get_docfield(doc.doctype, "product_bundle_help", doc.name).options = ""; } } @@ -416,7 +416,7 @@ erpnext.sales_common = { project() { let me = this; - if (in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) { + if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) { if (this.frm.doc.project) { frappe.call({ method: "erpnext.projects.doctype.project.project.get_cost_center_name", diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 24133b8cdc3..42d37bf493b 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -542,6 +542,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { frappe.throw(__("Please add atleast one Serial No / Batch No")); } + if (!warehouse) { + frappe.throw(__("Please select a Warehouse")); + } + frappe .call({ method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers", diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js index 5fbb5cb7e01..7aa8012f0b6 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.js @@ -34,7 +34,7 @@ frappe.ui.form.on("Import Supplier Invoice", { }, toggle_read_only_fields: function (frm) { - if (in_list(["File Import Completed", "Processing File Data"], frm.doc.status)) { + if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) { cur_frm.set_read_only(); cur_frm.refresh_fields(); frm.set_df_property("import_invoices", "hidden", 1); diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index db712d96b50..41c6311553c 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -583,7 +583,7 @@ "link_fieldname": "party" } ], - "modified": "2023-12-28 13:15:36.298369", + "modified": "2024-03-16 19:41:47.971815", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -661,7 +661,7 @@ } ], "quick_entry": 1, - "search_fields": "customer_name,customer_group,territory, mobile_no,primary_address", + "search_fields": "customer_group,territory, mobile_no,primary_address", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index a50d783d394..f55644380c7 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -370,37 +370,6 @@ class TestCustomer(FrappeTestCase): due_date = get_due_date("2017-01-22", "Customer", "_Test Customer") self.assertEqual(due_date, "2017-01-22") - def test_serach_fields_for_customer(self): - from erpnext.controllers.queries import customer_query - - frappe.db.set_single_value("Selling Settings", "cust_master_name", "Naming Series") - - make_property_setter( - "Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype" - ) - - data = customer_query( - "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True - ) - - self.assertEqual(data[0].name, "_Test Customer") - self.assertEqual(data[0].customer_group, "_Test Customer Group") - self.assertTrue("territory" not in data[0]) - - make_property_setter( - "Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype" - ) - data = customer_query( - "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True - ) - - self.assertEqual(data[0].name, "_Test Customer") - self.assertEqual(data[0].customer_group, "_Test Customer Group") - self.assertEqual(data[0].territory, "_Test Territory") - self.assertTrue("territory" in data[0]) - - frappe.db.set_single_value("Selling Settings", "cust_master_name", "Customer Name") - def test_parse_full_name(self): first, middle, last = parse_full_name("John") self.assertEqual(first, "John") diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 6e2b7262641..95cbfd0f32b 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +cur_frm.cscript.tax_table = "Sales Taxes and Charges"; + erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template"); erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges"); erpnext.pre_sales.set_as_lost("Quotation"); diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index d06acb81f1d..4ec01814b45 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -30,6 +30,39 @@ class TestQuotation(FrappeTestCase): self.assertTrue(sales_order.get("payment_schedule")) + def test_gross_profit(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + from erpnext.stock.get_item_details import insert_item_price + + item_doc = make_item("_Test Item for Gross Profit", {"is_stock_item": 1}) + item_code = item_doc.name + make_stock_entry(item_code=item_code, qty=10, rate=100, target="_Test Warehouse - _TC") + + selling_price_list = frappe.get_all("Price List", filters={"selling": 1}, limit=1)[0].name + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1) + insert_item_price( + frappe._dict( + { + "item_code": item_code, + "price_list": selling_price_list, + "price_list_rate": 300, + "rate": 300, + "conversion_factor": 1, + "discount_amount": 0.0, + "currency": frappe.db.get_value("Price List", selling_price_list, "currency"), + "uom": item_doc.stock_uom, + } + ) + ) + + quotation = make_quotation( + item_code=item_code, qty=1, rate=300, selling_price_list=selling_price_list + ) + self.assertEqual(quotation.items[0].valuation_rate, 100) + self.assertEqual(quotation.items[0].gross_profit, 200) + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + def test_maintain_rate_in_sales_cycle_is_enforced(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 2e3070c3927..10ba6a73fd7 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +cur_frm.cscript.tax_table = "Sales Taxes and Charges"; + erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges"); erpnext.accounts.taxes.setup_tax_validations("Sales Order"); erpnext.sales_common.setup_selling_controller(); diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 261566ec001..cce550270f0 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2091,6 +2091,40 @@ class TestSalesOrder(FrappeTestCase): dn.submit() dn.reload() + def test_auto_update_price_list(self): + item = make_item( + "_Test Auto Update Price List Item", + ) + + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1) + so = make_sales_order( + item_code=item.name, currency="USD", qty=1, rate=100, price_list_rate=100, do_not_submit=True + ) + so.save() + + item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate") + self.assertEqual(item_price, 100) + + so = make_sales_order( + item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=100, do_not_submit=True + ) + so.save() + + item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate") + self.assertEqual(item_price, 100) + + frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 1) + so = make_sales_order( + item_code=item.name, currency="USD", qty=1, rate=200, price_list_rate=200, do_not_submit=True + ) + so.save() + + item_price = frappe.db.get_value("Item Price", {"item_code": item.name}, "price_list_rate") + self.assertEqual(item_price, 200) + + frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0) + frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") @@ -2156,13 +2190,14 @@ def make_sales_order(**args): return so -def create_dn_against_so(so, delivered_qty=0): +def create_dn_against_so(so, delivered_qty=0, do_not_submit=False): frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) dn = make_delivery_note(so) dn.get("items")[0].qty = delivered_qty or 5 dn.insert() - dn.submit() + if not do_not_submit: + dn.submit() return dn diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index d95ef5893d9..fbee9c16267 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -295,10 +295,10 @@ erpnext.PointOfSale.ItemCart = class {
`); const me = this; - const query = { query: "erpnext.controllers.queries.customer_query" }; const allowed_customer_group = this.allowed_customer_groups || []; + let filters = {}; if (allowed_customer_group.length) { - query.filters = { + filters = { customer_group: ["in", allowed_customer_group], }; } @@ -308,7 +308,11 @@ erpnext.PointOfSale.ItemCart = class { fieldtype: "Link", options: "Customer", placeholder: __("Search by customer name, phone, email."), - get_query: () => query, + get_query: function () { + return { + filters: filters, + }; + }, onchange: function () { if (this.value) { const frm = me.events.get_frm(); diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 448dbcab43a..c399005643c 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -73,7 +73,7 @@ erpnext.PointOfSale.PastOrderSummary = class { const { status } = doc; let indicator_color = ""; - in_list(["Paid", "Consolidated"], status) && (indicator_color = "green"); + ["Paid", "Consolidated"].includes(status) && (indicator_color = "green"); status === "Draft" && (indicator_color = "red"); status === "Return" && (indicator_color = "grey"); diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index f2f1e4cfbaa..42bdf571738 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -197,6 +197,8 @@ def prepare_data( ): details[p_key] += r.get(qty_or_amount_field, 0) details[variance_key] = details.get(p_key) - details.get(target_key) + else: + details[variance_key] = details.get(p_key) - details.get(target_key) details["total_achieved"] += details.get(p_key) details["total_variance"] = details.get("total_achieved") - details.get("total_target") @@ -209,31 +211,32 @@ def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_fi parent_doc = frappe.qb.DocType(filters.get("doctype")) child_doc = frappe.qb.DocType(filters.get("doctype") + " Item") - sales_team = frappe.qb.DocType("Sales Team") - query = ( - frappe.qb.from_(parent_doc) - .inner_join(child_doc) - .on(child_doc.parent == parent_doc.name) - .inner_join(sales_team) - .on(sales_team.parent == parent_doc.name) - .select( - child_doc.item_group, - (child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"), - (child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"), - sales_team.sales_person, - parent_doc[date_field], - ) - .where( - (parent_doc.docstatus == 1) - & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date)) - ) - ) + query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name) if sales_field == "sales_person": - query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data)) + sales_team = frappe.qb.DocType("Sales Team") + stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100 + net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100 + sales_field_col = sales_team[sales_field] + + query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name) else: - query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data)) + stock_qty = child_doc.stock_qty + net_amount = child_doc.base_net_amount + sales_field_col = parent_doc[sales_field] + + query = query.select( + child_doc.item_group, + parent_doc[date_field], + (stock_qty).as_("stock_qty"), + (net_amount).as_("base_net_amount"), + sales_field_col, + ).where( + (parent_doc.docstatus == 1) + & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date)) + & (sales_field_col.isin(sales_users_or_territory_data)) + ) return query.run(as_dict=True) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py new file mode 100644 index 00000000000..17186687d97 --- /dev/null +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py @@ -0,0 +1,57 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt, nowdate + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.utils import get_fiscal_year +from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import ( + execute, +) +from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import ( + create_sales_target_doc, + create_target_distribution, +) + + +class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase): + def setUp(self): + self.fiscal_year = get_fiscal_year(nowdate())[0] + + def tearDown(self): + frappe.db.rollback() + + def test_achieved_target_and_variance_for_partner(self): + # Create a Target Distribution + distribution = create_target_distribution(self.fiscal_year) + + # Create Sales Partner with targets for the current fiscal year + sales_partner = create_sales_target_doc( + "Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name + ) + + # Create a Sales Invoice for the Partner + si = create_sales_invoice( + rate=1000, + qty=20, + do_not_submit=True, + ) + si.sales_partner = sales_partner + si.commission_rate = 5 + si.submit() + + # Check Achieved Target and Variance for the Sales Partner + result = execute( + frappe._dict( + { + "fiscal_year": self.fiscal_year, + "doctype": "Sales Invoice", + "period": "Yearly", + "target_on": "Quantity", + } + ) + )[1] + row = frappe._dict(result[0]) + self.assertSequenceEqual( + [flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)], + [50, 20, -30], + ) diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py index 4ae5d2bee88..73ae6d0c852 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py @@ -18,17 +18,17 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase): def test_achieved_target_and_variance(self): # Create a Target Distribution - distribution = frappe.new_doc("Monthly Distribution") - distribution.distribution_id = "Target Report Distribution" - distribution.fiscal_year = self.fiscal_year - distribution.get_months() - distribution.insert() + distribution = create_target_distribution(self.fiscal_year) - # Create sales people with targets - person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name) - person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name) + # Create sales people with targets for the current fiscal year + person_1 = create_sales_target_doc( + "Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name + ) + person_2 = create_sales_target_doc( + "Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name + ) - # Create a Sales Order with 50-50 contribution + # Create a Sales Order with 50-50 contribution between both Sales people so = make_sales_order( rate=1000, qty=20, @@ -69,10 +69,20 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase): ) -def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id): - sales_person = frappe.new_doc("Sales Person") - sales_person.sales_person_name = sales_person_name - sales_person.append( +def create_target_distribution(fiscal_year): + distribution = frappe.new_doc("Monthly Distribution") + distribution.distribution_id = "Target Report Distribution" + distribution.fiscal_year = fiscal_year + distribution.get_months() + return distribution.insert() + + +def create_sales_target_doc( + sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id +): + sales_target_doc = frappe.new_doc(sales_field_dt) + sales_target_doc.set(sales_field_name, sales_field_value) + sales_target_doc.append( "targets", { "fiscal_year": fiscal_year, @@ -81,4 +91,6 @@ def create_sales_person_with_target(sales_person_name, fiscal_year, distribution "distribution_id": distribution_id, }, ) - return sales_person.insert() + if sales_field_dt == "Sales Partner": + sales_target_doc.commission_rate = 5 + return sales_target_doc.insert() diff --git a/erpnext/setup/demo_data/item.json b/erpnext/setup/demo_data/item.json index 330e114dd53..17024341225 100644 --- a/erpnext/setup/demo_data/item.json +++ b/erpnext/setup/demo_data/item.json @@ -4,6 +4,7 @@ "item_group": "Demo Item Group", "item_code": "SKU001", "item_name": "T-shirt", + "valuation_rate": 400.0, "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/1484808/pexels-photo-1484808.jpeg" }, @@ -11,6 +12,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU002", + "valuation_rate": 300.0, "item_name": "Laptop", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/3999538/pexels-photo-3999538.jpeg" @@ -19,6 +21,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU003", + "valuation_rate": 523.0, "item_name": "Book", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/2422178/pexels-photo-2422178.jpeg" @@ -27,6 +30,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU004", + "valuation_rate": 725.0, "item_name": "Smartphone", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/1647976/pexels-photo-1647976.jpeg" @@ -35,6 +39,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU005", + "valuation_rate": 222.0, "item_name": "Sneakers", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg" @@ -43,6 +48,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU006", + "valuation_rate": 420.0, "item_name": "Coffee Mug", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/585753/pexels-photo-585753.jpeg" @@ -51,6 +57,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU007", + "valuation_rate": 375.0, "item_name": "Television", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/8059376/pexels-photo-8059376.jpeg" @@ -59,6 +66,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU008", + "valuation_rate": 333.0, "item_name": "Backpack", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/3731256/pexels-photo-3731256.jpeg" @@ -67,6 +75,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU009", + "valuation_rate": 700.0, "item_name": "Headphones", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/3587478/pexels-photo-3587478.jpeg" @@ -75,6 +84,7 @@ "doctype": "Item", "item_group": "Demo Item Group", "item_code": "SKU010", + "valuation_rate": 500.0, "item_name": "Camera", "gst_hsn_code": "999512", "image": "https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg" diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js index 0f0221fa562..aec752aec77 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Closing Stock Balance", { }, generate_closing_balance(frm) { - if (in_list(["Queued", "Failed"], frm.doc.status)) { + if (["Queued", "Failed"].includes(frm.doc.status)) { frm.add_custom_button(__("Generate Closing Stock Balance"), () => { frm.call({ method: "enqueue_job", diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index 1c7018366af..e99a0b1add2 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -123,7 +123,9 @@ class ClosingStockBalance(Document): ) ) - create_json_gz_file({"columns": columns, "data": data}, self.doctype, self.name) + create_json_gz_file( + {"columns": columns, "data": data}, self.doctype, self.name, "closing-stock-balance" + ) def get_prepared_data(self): if attachments := get_attachments(self.doctype, self.name): diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index c04d5c188a0..23d0adc5708 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -3,6 +3,8 @@ cur_frm.add_fetch("customer", "tax_id", "tax_id"); +cur_frm.cscript.tax_table = "Sales Taxes and Charges"; + frappe.provide("erpnext.stock"); frappe.provide("erpnext.stock.delivery_note"); frappe.provide("erpnext.accounts.dimensions"); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index fdc9753cfc7..4c2c023b6ae 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -251,6 +251,7 @@ class DeliveryNote(SellingController): def validate(self): self.validate_posting_time() super(DeliveryNote, self).validate() + self.validate_references() self.set_status() self.so_required() self.validate_proj_cust() @@ -333,6 +334,7 @@ class DeliveryNote(SellingController): "type_of_transaction": "Outward", "serial_and_batch_bundle": bundle_id, "item_code": item.get("item_code"), + "warehouse": item.get("warehouse"), } ) @@ -340,6 +342,58 @@ class DeliveryNote(SellingController): item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle + def validate_references(self): + self.validate_sales_order_references() + self.validate_sales_invoice_references() + + def validate_sales_order_references(self): + err_msg = "" + for item in self.items: + if (item.against_sales_order and not item.so_detail) or ( + not item.against_sales_order and item.so_detail + ): + if not item.against_sales_order: + err_msg += ( + _("'Sales Order' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("against_sales_order") + ) + + "
" + ) + else: + err_msg += ( + _("'Sales Order Item' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("so_detail") + ) + + "
" + ) + + if err_msg: + frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete")) + + def validate_sales_invoice_references(self): + err_msg = "" + for item in self.items: + if (item.against_sales_invoice and not item.si_detail) or ( + not item.against_sales_invoice and item.si_detail + ): + if not item.against_sales_invoice: + err_msg += ( + _("'Sales Invoice' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("against_sales_invoice") + ) + + "
" + ) + else: + err_msg += ( + _("'Sales Invoice Item' reference ({1}) is missing in row {0}").format( + frappe.bold(item.idx), frappe.bold("si_detail") + ) + + "
" + ) + + if err_msg: + frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete")) + def validate_proj_cust(self): """check for does customer belong to same project as entered..""" if self.project and self.customer: diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 80886294958..24544070d3b 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -813,6 +813,15 @@ class TestDeliveryNote(FrappeTestCase): dn.cancel() self.assertEqual(dn.status, "Cancelled") + def test_sales_order_reference_validation(self): + so = make_sales_order(po_no="12345") + dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True) + dn.items[0].against_sales_order = None + self.assertRaises(frappe.ValidationError, dn.save) + dn.reload() + dn.items[0].so_detail = None + self.assertRaises(frappe.ValidationError, dn.save) + def test_dn_billing_status_case1(self): # SO -> DN -> SI so = make_sales_order(po_no="12345") @@ -1088,9 +1097,30 @@ class TestDeliveryNote(FrappeTestCase): dn.load_from_db() batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle) + packed_name = dn.packed_items[0].name self.assertTrue(batch_no) + dn.cancel() + + # Cancel the reposting entry + reposting_entries = frappe.get_all("Repost Item Valuation", filters={"docstatus": 1}) + for entry in reposting_entries: + doc = frappe.get_doc("Repost Item Valuation", entry.name) + doc.cancel() + doc.delete() + + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1) + + dn.reload() + dn.delete() + + bundle = frappe.db.get_value( + "Serial and Batch Bundle", {"voucher_detail_no": packed_name}, "name" + ) + self.assertFalse(bundle) + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js b/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js index 230107caadb..65a1be33224 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip_list.js @@ -1,9 +1,9 @@ frappe.listview_settings["Delivery Trip"] = { add_fields: ["status"], get_indicator: function (doc) { - if (in_list(["Cancelled", "Draft"], doc.status)) { + if (["Cancelled", "Draft"].includes(doc.status)) { return [__(doc.status), "red", "status,=," + doc.status]; - } else if (in_list(["In Transit", "Scheduled"], doc.status)) { + } else if (["In Transit", "Scheduled"].includes(doc.status)) { return [__(doc.status), "orange", "status,=," + doc.status]; } else if (doc.status === "Completed") { return [__(doc.status), "green", "status,=," + doc.status]; diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 7a38024872e..5310a0f4d26 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -406,14 +406,6 @@ $.extend(erpnext.item, { }; }; - frm.fields_dict.customer_items.grid.get_field("customer_name").get_query = function (doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.customer_query" }; - }; - - frm.fields_dict.supplier_items.grid.get_field("supplier").get_query = function (doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.supplier_query" }; - }; - frm.fields_dict["item_defaults"].grid.get_field("default_warehouse").get_query = function ( doc, cdt, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 8a1f79d4a27..627520c1dcd 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -184,7 +184,11 @@ class PickList(Document): def delink_serial_and_batch_bundle(self): for row in self.locations: - if row.serial_and_batch_bundle: + if ( + row.serial_and_batch_bundle + and frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "docstatus") + == 1 + ): frappe.db.set_value( "Serial and Batch Bundle", row.serial_and_batch_bundle, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 997cdd0e56f..bfac4381a06 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -3,6 +3,8 @@ frappe.provide("erpnext.stock"); +cur_frm.cscript.tax_table = "Purchase Taxes and Charges"; + erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges"); erpnext.accounts.taxes.setup_tax_validations("Purchase Receipt"); erpnext.buying.setup_buying_controller(); diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8c3c1f750ab..b4fc464b534 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2522,6 +2522,280 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(row.serial_no, "\n".join(serial_nos[:2])) self.assertEqual(row.rejected_serial_no, serial_nos[2]) + def test_internal_transfer_with_serial_batch_items_and_their_valuation(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + batch_item_doc = make_item( + "_Test Batch Item For Stock Transfer", + {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BIFST-.####"}, + ) + + serial_item_doc = make_item( + "_Test Serial No Item For Stock Transfer", + {"has_serial_no": 1, "serial_no_series": "BT-BIFST-.####"}, + ) + + inward_entry = make_purchase_receipt( + item_code=batch_item_doc.name, + qty=10, + rate=150, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + use_serial_batch_fields=1, + do_not_submit=1, + ) + + inward_entry.append( + "items", + { + "item_code": serial_item_doc.name, + "qty": 15, + "rate": 250, + "item_name": serial_item_doc.item_name, + "conversion_factor": 1.0, + "uom": serial_item_doc.stock_uom, + "stock_uom": serial_item_doc.stock_uom, + "warehouse": "Stores - TCP1", + "use_serial_batch_fields": 1, + }, + ) + + inward_entry.submit() + inward_entry.reload() + + for row in inward_entry.items: + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_dn = create_delivery_note( + item_code=inward_entry.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=500, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle), + use_serial_batch_fields=1, + do_not_submit=1, + ) + + inter_transfer_dn.append( + "items", + { + "item_code": serial_item_doc.name, + "qty": 15, + "rate": 350, + "item_name": serial_item_doc.item_name, + "conversion_factor": 1.0, + "uom": serial_item_doc.stock_uom, + "stock_uom": serial_item_doc.stock_uom, + "warehouse": "Stores - TCP1", + "target_warehouse": "Work In Progress - TCP1", + "serial_no": "\n".join( + get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle) + ), + "use_serial_batch_fields": 1, + }, + ) + + inter_transfer_dn.submit() + inter_transfer_dn.reload() + for row in inter_transfer_dn.items: + if row.item_code == batch_item_doc.name: + self.assertEqual(row.rate, 150.0) + else: + self.assertEqual(row.rate, 250.0) + + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name) + for row in inter_transfer_pr.items: + row.from_warehouse = "Work In Progress - TCP1" + row.warehouse = "Stores - TCP1" + inter_transfer_pr.submit() + + for row in inter_transfer_pr.items: + if row.item_code == batch_item_doc.name: + self.assertEqual(row.rate, 150.0) + else: + self.assertEqual(row.rate, 250.0) + + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name) + + inter_transfer_pr_return.submit() + inter_transfer_pr_return.reload() + for row in inter_transfer_pr_return.items: + self.assertTrue(row.serial_and_batch_bundle) + if row.item_code == serial_item_doc.name: + self.assertEqual(row.rate, 250.0) + serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + for sn in serial_nos: + serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1) + self.assertTrue(serial_no_details.status == "Active") + self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1") + + inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name) + inter_transfer_dn_return.posting_date = today() + inter_transfer_dn_return.posting_time = nowtime() + for row in inter_transfer_dn_return.items: + row.target_warehouse = "Work In Progress - TCP1" + + inter_transfer_dn_return.submit() + inter_transfer_dn_return.reload() + + for row in inter_transfer_dn_return.items: + self.assertTrue(row.serial_and_batch_bundle) + + def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0) + + prepare_data_for_internal_transfer() + + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + batch_item_doc = make_item( + "_Test Batch Item For Stock Transfer USE SERIAL BATCH FIELDS", + {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF-BT-BIFST-.####"}, + ) + + serial_item_doc = make_item( + "_Test Serial No Item For Stock Transfer USE SERIAL BATCH FIELDS", + {"has_serial_no": 1, "serial_no_series": "USBF-BT-BIFST-.####"}, + ) + + inward_entry = make_purchase_receipt( + item_code=batch_item_doc.name, + qty=10, + rate=150, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + use_serial_batch_fields=0, + do_not_submit=1, + ) + + inward_entry.append( + "items", + { + "item_code": serial_item_doc.name, + "qty": 15, + "rate": 250, + "item_name": serial_item_doc.item_name, + "conversion_factor": 1.0, + "uom": serial_item_doc.stock_uom, + "stock_uom": serial_item_doc.stock_uom, + "warehouse": "Stores - TCP1", + "use_serial_batch_fields": 0, + }, + ) + + inward_entry.submit() + inward_entry.reload() + + for row in inward_entry.items: + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_dn = create_delivery_note( + item_code=inward_entry.items[0].item_code, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=500, + warehouse="Stores - TCP1", + target_warehouse="Work In Progress - TCP1", + batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle), + use_serial_batch_fields=0, + do_not_submit=1, + ) + + inter_transfer_dn.append( + "items", + { + "item_code": serial_item_doc.name, + "qty": 15, + "rate": 350, + "item_name": serial_item_doc.item_name, + "conversion_factor": 1.0, + "uom": serial_item_doc.stock_uom, + "stock_uom": serial_item_doc.stock_uom, + "warehouse": "Stores - TCP1", + "target_warehouse": "Work In Progress - TCP1", + "serial_no": "\n".join( + get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle) + ), + "use_serial_batch_fields": 0, + }, + ) + + inter_transfer_dn.submit() + inter_transfer_dn.reload() + for row in inter_transfer_dn.items: + if row.item_code == batch_item_doc.name: + self.assertEqual(row.rate, 150.0) + else: + self.assertEqual(row.rate, 250.0) + + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name) + for row in inter_transfer_pr.items: + row.from_warehouse = "Work In Progress - TCP1" + row.warehouse = "Stores - TCP1" + inter_transfer_pr.submit() + + for row in inter_transfer_pr.items: + if row.item_code == batch_item_doc.name: + self.assertEqual(row.rate, 150.0) + else: + self.assertEqual(row.rate, 250.0) + + self.assertTrue(row.serial_and_batch_bundle) + + inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name) + + inter_transfer_pr_return.submit() + inter_transfer_pr_return.reload() + for row in inter_transfer_pr_return.items: + self.assertTrue(row.serial_and_batch_bundle) + if row.item_code == serial_item_doc.name: + self.assertEqual(row.rate, 250.0) + serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle) + for sn in serial_nos: + serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1) + self.assertTrue(serial_no_details.status == "Active") + self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1") + + inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name) + inter_transfer_dn_return.posting_date = today() + inter_transfer_dn_return.posting_time = nowtime() + for row in inter_transfer_dn_return.items: + row.target_warehouse = "Work In Progress - TCP1" + + inter_transfer_dn_return.submit() + inter_transfer_dn_return.reload() + + for row in inter_transfer_dn_return.items: + self.assertTrue(row.serial_and_batch_bundle) + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 7a58462357b..59ef43e31a8 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -113,6 +113,7 @@ { "fieldname": "voucher_no", "fieldtype": "Dynamic Link", + "in_standard_filter": 1, "label": "Voucher No", "no_copy": 1, "options": "voucher_type", @@ -250,7 +251,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-07 17:56:55.528563", + "modified": "2024-03-15 15:22:24.003486", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", 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 08cb3ca3074..58971e8f19d 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 @@ -778,6 +778,10 @@ class SerialandBatchBundle(Document): or_filters=or_filters, ) + if not vouchers and self.voucher_type == "Delivery Note": + frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None) + return + for voucher in vouchers: if voucher.get("current_serial_and_batch_bundle"): frappe.db.set_value(self.child_table, voucher.name, "current_serial_and_batch_bundle", None) @@ -801,6 +805,7 @@ class SerialandBatchBundle(Document): self.set_purchase_document_no() def on_submit(self): + self.validate_batch_inventory() self.validate_serial_nos_inventory() def set_purchase_document_no(self): @@ -827,6 +832,13 @@ class SerialandBatchBundle(Document): if not self.has_batch_no: return + if ( + self.voucher_type == "Stock Reconciliation" + and self.type_of_transaction == "Outward" + and frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty") > 0 + ): + return + batches = [d.batch_no for d in self.entries if d.batch_no] if not batches: return diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4421a3e7938..c317a889ec6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2606,6 +2606,7 @@ def move_sample_to_retention_warehouse(company, items): "type_of_transaction": "Outward", "serial_and_batch_bundle": item.get("serial_and_batch_bundle"), "item_code": item.get("item_code"), + "warehouse": item.get("t_warehouse"), } ) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 902b8ffa90c..ce3c4958cec 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -999,6 +999,7 @@ class TestStockEntry(FrappeTestCase): "type_of_transaction": "Inward", "serial_and_batch_bundle": s2.items[0].serial_and_batch_bundle, "item_code": "_Test Serialized Item", + "warehouse": "_Test Warehouse - _TC", } ) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 3a094f1e8f5..e8e82af25ac 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -230,7 +230,7 @@ }, { "fieldname": "stock_queue", - "fieldtype": "Text", + "fieldtype": "Long Text", "label": "FIFO Stock Queue (qty, rate)", "oldfieldname": "fcfs_stack", "oldfieldtype": "Text", @@ -360,7 +360,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-02-07 09:18:13.999231", + "modified": "2024-03-13 09:56:13.021696", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", 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 186567a996a..e608e930276 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -58,7 +58,7 @@ class StockLedgerEntry(Document): recalculate_rate: DF.Check serial_and_batch_bundle: DF.Link | None serial_no: DF.LongText | None - stock_queue: DF.Text | None + stock_queue: DF.LongText | None stock_uom: DF.Link | None stock_value: DF.Currency stock_value_difference: DF.Currency diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3356ad5f300..0311481b6ca 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -154,7 +154,6 @@ class StockReconciliation(StockController): { "current_serial_and_batch_bundle": sn_doc.name, "current_serial_no": "", - "batch_no": "", } ) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 1cb10575cd1..85da0348265 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -816,7 +816,9 @@ def get_price_list_rate(args, item_doc, out=None): price_list_rate = get_price_list_rate_for(args, item_doc.variant_of) # insert in database - if price_list_rate is None: + if price_list_rate is None or frappe.db.get_single_value( + "Stock Settings", "update_existing_price_list_rate" + ): if args.price_list and args.rate: insert_item_price(args) return out diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 2e4b08c3ea5..e98351a0a8b 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -60,6 +60,7 @@ def execute(filters=None): if filters.get("batch_no") or inventory_dimension_filters_applied: actual_qty += flt(sle.actual_qty, precision) stock_value += sle.stock_value_difference + batch_balance_dict[sle.batch_no] += sle.actual_qty if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: actual_qty = sle.qty_after_transaction diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 4e87fa022d8..7b42103bdeb 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -820,6 +820,10 @@ class SerialBatchCreation: self.remove_returned_serial_nos(new_package) new_package.docstatus = 0 + new_package.warehouse = self.warehouse + new_package.voucher_no = "" + new_package.posting_date = today() + new_package.posting_time = nowtime() new_package.type_of_transaction = self.type_of_transaction new_package.returned_against = self.get("returned_against") new_package.save() diff --git a/erpnext/templates/form_grid/item_grid.html b/erpnext/templates/form_grid/item_grid.html index 027046fd92f..72db6c8e653 100644 --- a/erpnext/templates/form_grid/item_grid.html +++ b/erpnext/templates/form_grid/item_grid.html @@ -18,7 +18,7 @@ actual_qty = (frm.doc.doctype==="Sales Order" ? doc.projected_qty : doc.actual_qty); if(flt(frm.doc.per_delivered, 2) < 100 - && in_list(["Sales Order Item", "Delivery Note Item"], doc.doctype)) { + && ["Sales Order Item", "Delivery Note Item"].includes(doc.doctype)) { if(actual_qty != undefined) { if(actual_qty >= doc.qty) { var color = "green";