diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 0c71b41a7c..48337cee64 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev pip install frappe-bench +githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}} frappeuser=${FRAPPE_USER:-"frappe"} -frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}} +frappebranch=${FRAPPE_BRANCH:-$githubbranch} git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1 bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench @@ -60,7 +61,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile -bench get-app payments +bench get-app payments --branch ${githubbranch%"-hotfix"} bench get-app erpnext "${GITHUB_WORKSPACE}" if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index ec0ba081c8..0404d1c677 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda if ancestors and not allow_independent_account_creation: for ancestor in ancestors: - if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"): + old_name = frappe.db.get_value( + "Account", + {"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor}, + "name", + ) + + if old_name: # same account in parent company exists allow_child_account_creation = _("Allow Account Creation Against Child Company") diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index e3d9c26b2d..c9e3998ac8 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -221,12 +221,15 @@ class PaymentReconciliation(Document): def get_difference_amount(self, payment_entry, invoice, allocated_amount): difference_amount = 0 - if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( - "exchange_rate", 1 - ): - allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount - allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount - difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate + if frappe.get_cached_value( + "Account", self.receivable_payable_account, "account_currency" + ) != frappe.get_cached_value("Company", self.company, "default_currency"): + if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( + "exchange_rate", 1 + ): + allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount + allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount + difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate return difference_amount diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index f9dda0593b..3be11ae31a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -5,7 +5,7 @@ import unittest import frappe from frappe import qb -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, nowdate from erpnext import get_default_cost_center @@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase): 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})) + + # Difference amount should not be calculated for base currency accounts + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() si.reload() @@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase): 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})) + + # Difference amount should not be calculated for base currency accounts + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() # check PR tool output @@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase): 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})) + + # Difference amount should not be calculated for base currency accounts + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() # assert outstanding @@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase): 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})) + + # Difference amount should not be calculated for base currency accounts + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + pr.reconcile() self.assertEqual(pr.get("invoices"), []) @@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase): payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] self.assertCountEqual(payment_vouchers, [je2.name, pe2.name]) + @change_settings( + "Accounts Settings", + { + "allow_multi_currency_invoices_against_single_party_account": 1, + }, + ) + def test_no_difference_amount_for_base_currency_accounts(self): + # Make Sale Invoice + si = self.create_sales_invoice( + qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + si.customer = self.customer + si.currency = "EUR" + si.conversion_rate = 85 + si.debit_to = self.debit_to + si.save().submit() + + # Make payment using Payment Entry + pe1 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.bank, + paid_amount=100, + ) + + pe1.save() + pe1.submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer + pr.receivable_payable_account = self.debit_to + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 85) + self.assertEqual(pr.allocation[0].difference_amount, 0) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): 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 3920d4cf09..b9680dfb3b 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 @@ -15,7 +15,7 @@

{{ _("STATEMENTS OF ACCOUNTS") }}

-
{{ _("Customer: ") }} {{filters.party[0] }}
+
{{ _("Customer: ") }} {{filters.party_name[0] }}
{{ _("Date: ") }} {{ frappe.format(filters.from_date, 'Date')}} diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index a48c0272ff..a482931a8e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get class ProcessStatementOfAccounts(Document): def validate(self): if not self.subject: - self.subject = "Statement Of Accounts for {{ customer.name }}" + self.subject = "Statement Of Accounts for {{ customer.customer_name }}" if not self.body: self.body = "Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." @@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True): "account": [doc.account] if doc.account else None, "party_type": "Customer", "party": [entry.customer], + "party_name": [entry.customer_name] if entry.customer_name else None, "presentation_currency": presentation_currency, "group_by": doc.group_by, "currency": doc.currency, @@ -156,7 +157,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll ] return frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[[fields_dict[customer_collection], "IN", selected]], ) @@ -179,7 +180,7 @@ def get_customers_based_on_sales_person(sales_person): if sales_person_records.get("Customer"): return frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[["name", "in", list(sales_person_records["Customer"])]], ) else: @@ -228,7 +229,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): if customer_collection == "Sales Partner": customers = frappe.get_list( "Customer", - fields=["name", "email_id"], + fields=["name", "customer_name", "email_id"], filters=[["default_sales_partner", "=", collection_name]], ) else: @@ -245,7 +246,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory): continue customer_list.append( - {"name": customer.name, "primary_email": primary_email, "billing_email": billing_email} + { + "name": customer.name, + "customer_name": customer.customer_name, + "primary_email": primary_email, + "billing_email": billing_email, + } ) return customer_list diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json index dd04dc1b3c..8bffd6a93b 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json @@ -1,12 +1,12 @@ { "actions": [], - "allow_workflow": 1, "creation": "2020-08-03 16:35:21.852178", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "customer", + "customer_name", "billing_email", "primary_email" ], @@ -30,11 +30,18 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Billing Email" + }, + { + "fetch_from": "customer.customer_name", + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-08-03 22:55:38.875601", + "modified": "2023-03-13 00:12:34.508086", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts Customer", @@ -43,5 +50,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 2f4e45e618..2a8ff40413 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -32,9 +32,6 @@ "cost_center", "dimension_col_break", "project", - "column_break_27", - "campaign", - "source", "currency_and_price_list", "currency", "conversion_rate", @@ -203,7 +200,9 @@ "more_information", "status", "inter_company_invoice_reference", + "campaign", "represents_company", + "source", "customer_group", "col_break23", "is_internal_customer", @@ -2083,10 +2082,6 @@ "fieldname": "company_addr_col_break", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_27", - "fieldtype": "Column Break" - }, { "fieldname": "column_break_52", "fieldtype": "Column Break" @@ -2143,11 +2138,10 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-01-28 19:45:47.538163", + "modified": "2023-03-13 11:43:15.883055", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 0ffd9463e6..6051c9915d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -266,16 +266,16 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36], "_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39], "_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04], - "_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17], - "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55], + "_Test Account VAT - _TC": [156.0, 1808.0, 3.12, 36.16], + "_Test Account Discount - _TC": [-181.0, 1627.0, -3.62, 32.54], } for d in si.get("taxes"): for i, k in enumerate(expected_values["keys"]): self.assertEqual(d.get(k), expected_values[d.account_head][i]) - self.assertEqual(si.base_grand_total, 1627.5) - self.assertEqual(si.grand_total, 32.55) + self.assertEqual(si.base_grand_total, 1627.0) + self.assertEqual(si.grand_total, 32.54) def test_sales_invoice_with_discount_and_inclusive_tax(self): si = create_sales_invoice(qty=100, rate=50, do_not_save=True) @@ -401,10 +401,10 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67], "_Test Account CST - _TC": [27.88, 25.95, 1323.62], "_Test Account VAT - _TC": [156.25, 145.43, 1469.05], - "_Test Account Customs Duty - _TC": [125, 116.35, 1585.40], - "_Test Account Shipping Charges - _TC": [100, 100, 1685.40], - "_Test Account Discount - _TC": [-180.33, -168.54, 1516.86], - "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01], + "_Test Account Customs Duty - _TC": [125, 116.34, 1585.39], + "_Test Account Shipping Charges - _TC": [100, 100, 1685.39], + "_Test Account Discount - _TC": [-180.33, -168.54, 1516.85], + "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.00], } for d in si.get("taxes"): @@ -413,7 +413,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.base_grand_total, 1500) self.assertEqual(si.grand_total, 1500) - self.assertEqual(si.rounding_adjustment, -0.01) + self.assertEqual(si.rounding_adjustment, 0.0) def test_discount_amount_gl_entry(self): frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") @@ -454,7 +454,7 @@ class TestSalesInvoice(unittest.TestCase): [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30], [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95], [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43], - [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35], + [test_records[3]["taxes"][5]["account_head"], 0.0, 116.34], [test_records[3]["taxes"][6]["account_head"], 0.0, 100], [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0], ["_Test Account Service Tax - _TC", 16.85, 0.0], @@ -1614,7 +1614,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account Education Cess - _TC": [1.4, 1.4, 1.4], "_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7], "_Test Account CST - _TC": [17.19, 17.19, 17.19], - "_Test Account VAT - _TC": [78.13, 78.13, 78.13], + "_Test Account VAT - _TC": [78.12, 78.12, 78.12], "_Test Account Discount - _TC": [-95.49, -95.49, -95.49], } @@ -1623,9 +1623,9 @@ class TestSalesInvoice(unittest.TestCase): if expected_values.get(d.account_head): self.assertEqual(d.get(k), expected_values[d.account_head][i]) - self.assertEqual(si.total_taxes_and_charges, 234.43) - self.assertEqual(si.base_grand_total, 859.43) - self.assertEqual(si.grand_total, 859.43) + self.assertEqual(si.total_taxes_and_charges, 234.42) + self.assertEqual(si.base_grand_total, 859.42) + self.assertEqual(si.grand_total, 859.42) def test_multi_currency_gle(self): si = create_sales_invoice( @@ -1985,17 +1985,17 @@ class TestSalesInvoice(unittest.TestCase): ) si.save() si.submit() - self.assertEqual(si.net_total, 19453.13) + self.assertEqual(si.net_total, 19453.12) self.assertEqual(si.grand_total, 24900) self.assertEqual(si.total_taxes_and_charges, 5446.88) - self.assertEqual(si.rounding_adjustment, -0.01) + self.assertEqual(si.rounding_adjustment, 0.0) expected_values = dict( (d[0], d) for d in [ [si.debit_to, 24900, 0.0], ["_Test Account Service Tax - _TC", 0.0, 5446.88], - ["Sales - _TC", 0.0, 19453.13], + ["Sales - _TC", 0.0, 19453.12], ["Round Off - _TC", 0.01, 0.0], ] ) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 01cfb58dec..b217f00065 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -32,6 +32,16 @@ from erpnext import get_company_currency from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen +PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"} +SALES_TRANSACTION_TYPES = { + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + "POS Invoice", +} +TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES + class DuplicatePartyAccountError(frappe.ValidationError): pass @@ -124,12 +134,6 @@ def _get_party_details( set_other_values(party_details, party, party_type) set_price_list(party_details, party, party_type, price_list, pos_profile) - party_details["tax_category"] = get_address_tax_category( - party.get("tax_category"), - party_address, - shipping_address if party_type != "Supplier" else party_address, - ) - tax_template = set_taxes( party.name, party_type, @@ -211,20 +215,10 @@ def set_address_details( else: party_details.update(get_company_address(company)) - if doctype and doctype in [ - "Delivery Note", - "Sales Invoice", - "Sales Order", - "Quotation", - "POS Invoice", - ]: - if party_details.company_address: - party_details.update( - get_fetch_values(doctype, "company_address", party_details.company_address) - ) - get_regional_address_details(party_details, doctype, company) + if doctype in SALES_TRANSACTION_TYPES and party_details.company_address: + party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address)) - elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: + if doctype in PURCHASE_TRANSACTION_TYPES: if shipping_address: party_details.update( shipping_address=shipping_address, @@ -250,9 +244,21 @@ def set_address_details( **get_fetch_values(doctype, "shipping_address", party_details.billing_address) ) + party_address, shipping_address = ( + party_details.get(billing_address_field), + party_details.shipping_address_name, + ) + + party_details["tax_category"] = get_address_tax_category( + party.get("tax_category"), + party_address, + shipping_address if party_type != "Supplier" else party_address, + ) + + if doctype in TRANSACTION_TYPES: get_regional_address_details(party_details, doctype, company) - return party_details.get(billing_address_field), party_details.shipping_address_name + return party_address, shipping_address @erpnext.allow_regional diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 94a1510f09..11de9a098d 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -859,7 +859,7 @@ class ReceivablePayableReport(object): ) else: self.qb_selection_filter.append( - self.ple[dimension.fieldname] == self.filters[dimension.fieldname] + self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname]) ) def is_invoice(self, ple): diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index fde4de8402..01fee281b0 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -501,7 +501,14 @@ class GrossProfitGenerator(object): ): returned_item_rows = self.returned_invoices[row.parent][row.item_code] for returned_item_row in returned_item_rows: - row.qty += flt(returned_item_row.qty) + # returned_items 'qty' should be stateful + if returned_item_row.qty != 0: + if row.qty >= abs(returned_item_row.qty): + row.qty += returned_item_row.qty + returned_item_row.qty = 0 + else: + row.qty = 0 + returned_item_row.qty += row.qty row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) if flt(row.qty) or row.base_amount: @@ -734,6 +741,8 @@ class GrossProfitGenerator(object): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" + conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" + if self.filters.item_group: conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 21681bef5b..82fe1a0ba1 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase): } gp_entry = [x for x in data if x.parent_invoice == sinv.name] self.assertDictContainsSubset(expected_entry, gp_entry[0]) + + def test_crnote_against_invoice_with_multiple_instances_of_same_item(self): + """ + Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items. + """ + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + # Invoice with an item added twice + sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True) + sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False)) + sinv = sinv.save().submit() + + # Create Credit Note for Invoice + cr_note = make_sales_return(sinv.name) + cr_note = cr_note.save().submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 0.0, + "avg._selling_rate": 0.0, + "valuation_rate": 0.0, + "selling_amount": -100.0, + "buying_amount": 0.0, + "gross_profit": -100.0, + "gross_profit_%": 100.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + # Both items of Invoice should have '0' qty + self.assertEqual(len(gp_entry), 2) + self.assertDictContainsSubset(expected_entry, gp_entry[0]) + self.assertDictContainsSubset(expected_entry, gp_entry[1]) + + def test_standalone_cr_notes(self): + """ + Standalone cr notes will be reported as usual + """ + # Make Cr Note + sinv = self.create_sales_invoice( + qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + sinv.is_return = 1 + sinv = sinv.save().submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": -1.0, + "avg._selling_rate": 100.0, + "valuation_rate": 0.0, + "selling_amount": -100.0, + "buying_amount": 0.0, + "gross_profit": -100.0, + "gross_profit_%": 100.0, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry, gp_entry[0]) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 3af01fde7d..bc334c7060 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -78,7 +78,6 @@ def validate_filters(filters): def get_data(filters): - accounts = frappe.db.sql( """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt @@ -118,12 +117,10 @@ def get_data(filters): ignore_closing_entries=not flt(filters.with_period_closing_entry), ) - total_row = calculate_values( - accounts, gl_entries_by_account, opening_balances, filters, company_currency - ) + calculate_values(accounts, gl_entries_by_account, opening_balances) accumulate_values_into_parents(accounts, accounts_by_name) - data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency) + data = prepare_data(accounts, filters, parent_children_map, company_currency) data = filter_out_zero_value_rows( data, parent_children_map, show_zero_values=filters.get("show_zero_values") ) @@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type): return opening -def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency): +def calculate_values(accounts, gl_entries_by_account, opening_balances): init = { "opening_debit": 0.0, "opening_credit": 0.0, @@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, "closing_credit": 0.0, } - total_row = { - "account": "'" + _("Total") + "'", - "account_name": "'" + _("Total") + "'", - "warn_if_negative": True, - "opening_debit": 0.0, - "opening_credit": 0.0, - "debit": 0.0, - "credit": 0.0, - "closing_debit": 0.0, - "closing_credit": 0.0, - "parent_account": None, - "indent": 0, - "has_value": True, - "currency": company_currency, - } - for d in accounts: d.update(init.copy()) @@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, prepare_opening_closing(d) - for field in value_fields: - total_row[field] += d[field] + +def calculate_total_row(accounts, company_currency): + total_row = { + "account": "'" + _("Total") + "'", + "account_name": "'" + _("Total") + "'", + "warn_if_negative": True, + "opening_debit": 0.0, + "opening_credit": 0.0, + "debit": 0.0, + "credit": 0.0, + "closing_debit": 0.0, + "closing_credit": 0.0, + "parent_account": None, + "indent": 0, + "has_value": True, + "currency": company_currency, + } + + for d in accounts: + if not d.parent_account: + for field in value_fields: + total_row[field] += d[field] return total_row @@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name): accounts_by_name[d.parent_account][key] += d[key] -def prepare_data(accounts, filters, total_row, parent_children_map, company_currency): +def prepare_data(accounts, filters, parent_children_map, company_currency): data = [] for d in accounts: @@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr row["has_value"] = has_value data.append(row) + total_row = calculate_total_row(accounts, company_currency) data.extend([{}, total_row]) return data diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2608c03ffe..005a2f176c 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -455,7 +455,9 @@ def reconcile_against_document(args): # nosemgrep try: doc.validate_total_debit_and_credit() except Exception as validation_exception: - raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception + raise frappe.ValidationError( + _("Validation Error for {0}").format(doc.name) + ) from validation_exception doc.save(ignore_permissions=True) # re-submit advance entry diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 9a152638f9..2c9772d12a 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -828,8 +828,8 @@ class TestDepreciationMethods(AssetSetup): expected_schedules = [ ["2030-12-31", 28630.14, 28630.14], ["2031-12-31", 35684.93, 64315.07], - ["2032-12-31", 17842.47, 82157.54], - ["2033-06-06", 5342.46, 87500.0], + ["2032-12-31", 17842.46, 82157.53], + ["2033-06-06", 5342.47, 87500.0], ] schedules = [ diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index b75fbcbeb3..5912329846 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -140,8 +140,8 @@ class AssetDepreciationSchedule(Document): self.asset = asset_doc.name self.finance_book = row.finance_book self.finance_book_id = row.idx - self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation - self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked + self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0 + self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0 self.gross_purchase_amount = asset_doc.gross_purchase_amount self.depreciation_method = row.depreciation_method self.total_number_of_depreciations = row.total_number_of_depreciations @@ -185,14 +185,14 @@ class AssetDepreciationSchedule(Document): ): asset_doc.validate_asset_finance_books(row) - value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) + value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(asset_doc, row) row.value_after_depreciation = value_after_depreciation if update_asset_finance_book_row: row.db_update() number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( - asset_doc.number_of_depreciations_booked + self.number_of_depreciations_booked ) has_pro_rata = asset_doc.check_is_pro_rata(row) @@ -235,13 +235,12 @@ class AssetDepreciationSchedule(Document): self.add_depr_schedule_row( date_of_disposal, depreciation_amount, - row.depreciation_method, ) break # For first row - if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0: + if has_pro_rata and not self.opening_accumulated_depreciation and n == 0: from_date = add_days( asset_doc.available_for_use_date, -1 ) # needed to calc depr amount for available_for_use_date too @@ -260,7 +259,7 @@ class AssetDepreciationSchedule(Document): # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission asset_doc.to_date = add_months( asset_doc.available_for_use_date, - (n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), + (n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), ) depreciation_amount_without_pro_rata = depreciation_amount @@ -298,7 +297,6 @@ class AssetDepreciationSchedule(Document): self.add_depr_schedule_row( schedule_date, depreciation_amount, - row.depreciation_method, ) # to ensure that final accumulated depreciation amount is accurate @@ -325,14 +323,12 @@ class AssetDepreciationSchedule(Document): self, schedule_date, depreciation_amount, - depreciation_method, ): self.append( "depreciation_schedule", { "schedule_date": schedule_date, "depreciation_amount": depreciation_amount, - "depreciation_method": depreciation_method, }, ) @@ -346,7 +342,7 @@ class AssetDepreciationSchedule(Document): straight_line_idx = [ d.idx for d in self.get("depreciation_schedule") - if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual" + if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual" ] accumulated_depreciation = flt(self.opening_accumulated_depreciation) @@ -377,16 +373,15 @@ class AssetDepreciationSchedule(Document): accumulated_depreciation, d.precision("accumulated_depreciation_amount") ) + def _get_value_after_depreciation_for_making_schedule(self, asset_doc, fb_row): + if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: + value_after_depreciation = flt(fb_row.value_after_depreciation) + else: + value_after_depreciation = flt(self.gross_purchase_amount) - flt( + self.opening_accumulated_depreciation + ) -def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row): - if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: - value_after_depreciation = flt(fb_row.value_after_depreciation) - else: - value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt( - asset_doc.opening_accumulated_depreciation - ) - - return value_after_depreciation + return value_after_depreciation def make_draft_asset_depr_schedules_if_not_present(asset_doc): diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json index 882c4bf00b..abe295c680 100644 --- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json +++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json @@ -12,8 +12,7 @@ "column_break_3", "accumulated_depreciation_amount", "journal_entry", - "make_depreciation_entry", - "depreciation_method" + "make_depreciation_entry" ], "fields": [ { @@ -58,20 +57,11 @@ "fieldname": "make_depreciation_entry", "fieldtype": "Button", "label": "Make Depreciation Entry" - }, - { - "fieldname": "depreciation_method", - "fieldtype": "Select", - "hidden": 1, - "label": "Depreciation Method", - "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", - "print_hide": 1, - "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2022-12-06 20:35:50.264281", + "modified": "2023-03-13 23:17:15.849950", "modified_by": "Administrator", "module": "Assets", "name": "Depreciation Schedule", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 95857e4604..8c73e56a99 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -16,6 +16,7 @@ "transaction_settings_section", "po_required", "pr_required", + "over_order_allowance", "column_break_12", "maintain_same_rate", "set_landed_cost_based_on_purchase_invoice_rate", @@ -156,6 +157,13 @@ "fieldname": "set_landed_cost_based_on_purchase_invoice_rate", "fieldtype": "Check", "label": "Set Landed Cost Based on Purchase Invoice Rate" + }, + { + "default": "0", + "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", + "fieldname": "over_order_allowance", + "fieldtype": "Float", + "label": "Over Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -163,7 +171,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-28 15:41:32.686805", + "modified": "2023-03-02 17:02:14.404622", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2415aec8cb..06b9d29e69 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category from erpnext.accounts.party import get_party_account, get_party_account_currency from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController +from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( + validate_against_blanket_order, +) from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty @@ -69,6 +72,7 @@ class PurchaseOrder(BuyingController): self.validate_with_previous_doc() self.validate_for_subcontracting() self.validate_minimum_order_qty() + validate_against_blanket_order(self) if self.is_old_subcontracting_flow: self.validate_bom_for_subcontracting_items() diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 7927beb823..4590f8c3d9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController): def get_link(self): # RFQ link for supplier portal - return get_url("/app/request-for-quotation/" + self.name) + route = frappe.db.get_value( + "Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"] + ) + return get_url("/app/{0}/".format(route) + self.name) def update_supplier_part_no(self, supplier): self.vendor = supplier diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index bbe04d5514..e41c9da594 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -226,11 +226,11 @@ class TestWebsiteItem(unittest.TestCase): self.assertTrue(bool(data.product_info["price"])) price_object = data.product_info["price"] - self.assertEqual(price_object.get("discount_percent"), 25) + self.assertEqual(price_object.get("discount_percent"), 25.0) self.assertEqual(price_object.get("price_list_rate"), 750) self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00") self.assertEqual(price_object.get("formatted_price"), "₹ 750.00") - self.assertEqual(price_object.get("formatted_discount_percent"), "25%") + self.assertEqual(price_object.get("formatted_discount_percent"), "25.0%") # switch to admin and disable show price frappe.set_user("Administrator") diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js index d3bb33e86e..7b26a14a57 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.js +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.js @@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', { }, setup: function(frm) { + frm.custom_make_buttons = { + 'Purchase Order': 'Purchase Order', + 'Sales Order': 'Sales Order', + 'Quotation': 'Quotation', + }; + frm.add_fetch("customer", "customer_name", "customer_name"); frm.add_fetch("supplier", "supplier_name", "supplier_name"); }, diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index ff2140199d..32f1c365ad 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import flt, getdate from erpnext.stock.doctype.item.item import get_item_defaults @@ -29,21 +30,23 @@ class BlanketOrder(Document): def update_ordered_qty(self): ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order" + + trans = frappe.qb.DocType(ref_doctype) + trans_item = frappe.qb.DocType(f"{ref_doctype} Item") + item_ordered_qty = frappe._dict( - frappe.db.sql( - """ - select trans_item.item_code, sum(trans_item.stock_qty) as qty - from `tab{0} Item` trans_item, `tab{0}` trans - where trans.name = trans_item.parent - and trans_item.blanket_order=%s - and trans.docstatus=1 - and trans.status not in ('Closed', 'Stopped') - group by trans_item.item_code - """.format( - ref_doctype - ), - self.name, - ) + ( + frappe.qb.from_(trans_item) + .from_(trans) + .select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty")) + .where( + (trans.name == trans_item.parent) + & (trans_item.blanket_order == self.name) + & (trans.docstatus == 1) + & (trans.status.notin(["Stopped", "Closed"])) + ) + .groupby(trans_item.item_code) + ).run() ) for d in self.items: @@ -79,7 +82,43 @@ def make_order(source_name): "doctype": doctype + " Item", "field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"}, "postprocess": update_item, + "condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0, }, }, ) return target_doc + + +def validate_against_blanket_order(order_doc): + if order_doc.doctype in ("Sales Order", "Purchase Order"): + order_data = {} + + for item in order_doc.get("items"): + if item.against_blanket_order and item.blanket_order: + if item.blanket_order in order_data: + if item.item_code in order_data[item.blanket_order]: + order_data[item.blanket_order][item.item_code] += item.qty + else: + order_data[item.blanket_order][item.item_code] = item.qty + else: + order_data[item.blanket_order] = {item.item_code: item.qty} + + if order_data: + allowance = flt( + frappe.db.get_single_value( + "Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", + "over_order_allowance", + ) + ) + for bo_name, item_data in order_data.items(): + bo_doc = frappe.get_doc("Blanket Order", bo_name) + for item in bo_doc.get("items"): + if item.item_code in item_data: + remaining_qty = item.qty - item.ordered_qty + allowed_qty = remaining_qty + (remaining_qty * (allowance / 100)) + if allowed_qty < item_data[item.item_code]: + frappe.throw( + _("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format( + item.item_code, allowed_qty, bo_name + ) + ) diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index 2f1f3ae0f5..58f3c95059 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase): po1.currency = get_company_currency(po1.company) self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) + def test_over_order_allowance(self): + # Sales Order + bo = make_blanket_order(blanket_order_type="Selling", quantity=100) + + frappe.flags.args.doctype = "Sales Order" + so = make_order(bo.name) + so.currency = get_company_currency(so.company) + so.delivery_date = today() + so.items[0].qty = 110 + self.assertRaises(frappe.ValidationError, so.submit) + + frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) + so.submit() + + # Purchase Order + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100) + + frappe.flags.args.doctype = "Purchase Order" + po = make_order(bo.name) + po.currency = get_company_currency(po.company) + po.schedule_date = today() + po.items[0].qty = 110 + self.assertRaises(frappe.ValidationError, po.submit) + + frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) + po.submit() + def make_blanket_order(**args): args = frappe._dict(args) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8ab79e68be..619a415c8b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -31,7 +31,7 @@ class BOMTree: # specifying the attributes to save resources # ref: https://docs.python.org/3/reference/datamodel.html#slots - __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + __slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"] def __init__( self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 @@ -50,9 +50,10 @@ class BOMTree: def __create_tree(self): bom = frappe.get_cached_doc("BOM", self.name) self.item_code = bom.item + self.bom_qty = bom.quantity for item in bom.get("items", []): - qty = item.qty / bom.quantity # quantity per unit + qty = item.stock_qty / bom.quantity # quantity per unit exploded_qty = self.exploded_qty * qty if item.bom_no: child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index d60feb2b39..01bf2e4315 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -6,7 +6,7 @@ from collections import deque from functools import partial import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, timeout from frappe.utils import cstr, flt from erpnext.controllers.tests.test_subcontracting_controller import ( @@ -27,6 +27,7 @@ test_dependencies = ["Item", "Quality Inspection Template"] class TestBOM(FrappeTestCase): + @timeout def test_get_items(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict @@ -37,6 +38,7 @@ class TestBOM(FrappeTestCase): self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict) self.assertEqual(len(items_dict.values()), 2) + @timeout def test_get_items_exploded(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict @@ -49,11 +51,13 @@ class TestBOM(FrappeTestCase): self.assertTrue(test_records[0]["items"][1]["item_code"] in items_dict) self.assertEqual(len(items_dict.values()), 3) + @timeout def test_get_items_list(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3) + @timeout def test_default_bom(self): def _get_default_bom_in_item(): return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom")) @@ -71,6 +75,7 @@ class TestBOM(FrappeTestCase): self.assertTrue(_get_default_bom_in_item(), bom.name) + @timeout def test_update_bom_cost_in_all_boms(self): # get current rate for '_Test Item 2' bom_rates = frappe.db.get_values( @@ -99,6 +104,7 @@ class TestBOM(FrappeTestCase): ): self.assertEqual(d.base_rate, rm_base_rate + 10) + @timeout def test_bom_cost(self): bom = frappe.copy_doc(test_records[2]) bom.insert() @@ -127,6 +133,7 @@ class TestBOM(FrappeTestCase): self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) + @timeout def test_bom_cost_with_batch_size(self): bom = frappe.copy_doc(test_records[2]) bom.docstatus = 0 @@ -145,6 +152,7 @@ class TestBOM(FrappeTestCase): self.assertAlmostEqual(bom.operating_cost, op_cost / 2) bom.delete() + @timeout def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)): @@ -181,6 +189,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.base_raw_material_cost, 27000) self.assertEqual(bom.base_total_cost, 33000) + @timeout def test_bom_cost_multi_uom_based_on_valuation_rate(self): bom = frappe.copy_doc(test_records[2]) bom.set_rate_of_sub_assembly_item_based_on_bom = 0 @@ -202,6 +211,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.items[0].rate, 20) + @timeout def test_bom_cost_with_fg_based_operating_cost(self): bom = frappe.copy_doc(test_records[4]) bom.insert() @@ -229,6 +239,7 @@ class TestBOM(FrappeTestCase): self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) + @timeout def test_subcontractor_sourced_item(self): item_code = "_Test Subcontracted FG Item 1" set_backflush_based_on("Material Transferred for Subcontract") @@ -310,6 +321,7 @@ class TestBOM(FrappeTestCase): supplied_items = sorted([d.rm_item_code for d in sco.supplied_items]) self.assertEqual(bom_items, supplied_items) + @timeout def test_bom_tree_representation(self): bom_tree = { "Assembly": { @@ -335,6 +347,7 @@ class TestBOM(FrappeTestCase): for reqd_item, created_item in zip(reqd_order, created_order): self.assertEqual(reqd_item, created_item.item_code) + @timeout def test_generated_variant_bom(self): from erpnext.controllers.item_variant import create_variant @@ -375,6 +388,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(reqd_item.qty, created_item.qty) self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) + @timeout def test_bom_recursion_1st_level(self): """BOM should not allow BOM item again in child""" item_code = make_item(properties={"is_stock_item": 1}).name @@ -387,6 +401,7 @@ class TestBOM(FrappeTestCase): bom.items[0].bom_no = bom.name bom.save() + @timeout def test_bom_recursion_transitive(self): item1 = make_item(properties={"is_stock_item": 1}).name item2 = make_item(properties={"is_stock_item": 1}).name @@ -408,6 +423,7 @@ class TestBOM(FrappeTestCase): bom1.save() bom2.save() + @timeout def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() @@ -421,6 +437,7 @@ class TestBOM(FrappeTestCase): # Items with whole UOMs can't be PL Items self.assertRaises(frappe.ValidationError, bom_doc.submit) + @timeout def test_bom_item_query(self): query = partial( item_query, @@ -440,6 +457,7 @@ class TestBOM(FrappeTestCase): ) self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") + @timeout def test_exclude_exploded_items_from_bom(self): bom_no = get_default_bom() new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no)) @@ -458,6 +476,7 @@ class TestBOM(FrappeTestCase): new_bom.delete() + @timeout def test_valid_transfer_defaults(self): bom_with_op = frappe.db.get_value( "BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1} @@ -489,11 +508,13 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.transfer_material_against, "Work Order") bom.delete() + @timeout def test_bom_name_length(self): """test >140 char names""" bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}} create_nested_bom(bom_tree, prefix="") + @timeout def test_version_index(self): bom = frappe.new_doc("BOM") @@ -515,6 +536,7 @@ class TestBOM(FrappeTestCase): msg=f"Incorrect index for {existing_boms}", ) + @timeout def test_bom_versioning(self): bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}} bom = create_nested_bom(bom_tree, prefix="") @@ -547,6 +569,7 @@ class TestBOM(FrappeTestCase): self.assertNotEqual(amendment.name, version.name) self.assertEqual(int(version.name.split("-")[-1]), 2) + @timeout def test_clear_inpection_quality(self): bom = frappe.copy_doc(test_records[2], ignore_no_copy=True) @@ -565,6 +588,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.quality_inspection_template, None) + @timeout def test_bom_pricing_based_on_lpp(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -585,6 +609,7 @@ class TestBOM(FrappeTestCase): bom.submit() self.assertEqual(bom.items[0].rate, 42) + @timeout def test_set_default_bom_for_item_having_single_bom(self): from erpnext.stock.doctype.item.test_item import make_item @@ -621,6 +646,7 @@ class TestBOM(FrappeTestCase): bom.reload() self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name) + @timeout def test_exploded_items_rate(self): rm_item = make_item( properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} @@ -649,6 +675,7 @@ class TestBOM(FrappeTestCase): bom.submit() self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + @timeout def test_bom_cost_update_flag(self): rm_item = make_item( properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 5dd557f8ab..2026f62914 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, timeout from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( update_cost_in_all_boms_in_test, @@ -20,6 +20,7 @@ class TestBOMUpdateTool(FrappeTestCase): def tearDown(self): frappe.db.rollback() + @timeout def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" @@ -33,6 +34,7 @@ class TestBOMUpdateTool(FrappeTestCase): self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1})) self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1})) + @timeout def test_bom_cost(self): for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: item_doc = create_item(item, valuation_rate=100) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index ae9e9c6962..66b871c746 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -682,7 +682,7 @@ class WorkOrder(Document): for node in bom_traversal: if node.is_bom: - operations.extend(_get_operations(node.name, qty=node.exploded_qty)) + operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty)) bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity") operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e4995f8203..d42b012466 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -322,6 +322,8 @@ erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers erpnext.patches.v14_0.set_pick_list_status +erpnext.patches.v13_0.update_docs_link erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries +erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch # below migration patches should always run last -erpnext.patches.v14_0.migrate_gl_to_payment_ledger +erpnext.patches.v14_0.migrate_gl_to_payment_ledger \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_docs_link.py b/erpnext/patches/v13_0/update_docs_link.py new file mode 100644 index 0000000000..4bc5c053d2 --- /dev/null +++ b/erpnext/patches/v13_0/update_docs_link.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + + +import frappe + + +def execute(): + navbar_settings = frappe.get_single("Navbar Settings") + for item in navbar_settings.help_dropdown: + if item.is_standard and item.route == "https://erpnext.com/docs/user/manual": + item.route = "https://docs.erpnext.com/docs/v14/user/manual/en/introduction" + + navbar_settings.save() diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py index b803e9fa2d..af736919d8 100644 --- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py +++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py @@ -7,6 +7,9 @@ from erpnext.setup.utils import get_exchange_rate def execute(): + frappe.reload_doc( + "accounts", "doctype", "currency_exchange_settings" + ) # get_exchange_rate depends on Currency Exchange Settings frappe.reload_doctype("Opportunity") opportunities = frappe.db.get_list( "Opportunity", diff --git a/erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py b/erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py new file mode 100644 index 0000000000..afb59e0f6f --- /dev/null +++ b/erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py @@ -0,0 +1,20 @@ +import frappe + + +def execute(): + # not using frappe.qb because https://github.com/frappe/frappe/issues/20292 + frappe.db.sql( + """UPDATE `tabAsset Depreciation Schedule` + JOIN `tabAsset` + ON `tabAsset Depreciation Schedule`.`asset`=`tabAsset`.`name` + SET + `tabAsset Depreciation Schedule`.`gross_purchase_amount`=`tabAsset`.`gross_purchase_amount`, + `tabAsset Depreciation Schedule`.`number_of_depreciations_booked`=`tabAsset`.`number_of_depreciations_booked` + WHERE + ( + `tabAsset Depreciation Schedule`.`gross_purchase_amount`<>`tabAsset`.`gross_purchase_amount` + OR + `tabAsset Depreciation Schedule`.`number_of_depreciations_booked`<>`tabAsset`.`number_of_depreciations_booked` + ) + AND `tabAsset Depreciation Schedule`.`docstatus`<2""" + ) diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index a376bf46a5..d1d07a79d6 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -5,6 +5,8 @@ frappe.ui.form.on("Timesheet", { setup: function(frm) { frappe.require("/assets/erpnext/js/projects/timer.js"); + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice']; + frm.fields_dict.employee.get_query = function() { return { filters:{ diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py index 17e3155e28..766e40e319 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py @@ -46,6 +46,9 @@ def get_data(filters): # task has no end date, hence no delay task.delay = 0 + task.status = _(task.status) + task.priority = _(task.priority) + # Sort by descending order of delay tasks.sort(key=lambda x: x["delay"], reverse=True) return tasks @@ -73,7 +76,7 @@ def get_chart_data(data): on_track = on_track + 1 charts = { "data": { - "labels": ["On Track", "Delayed"], + "labels": [_("On Track"), _("Delayed")], "datasets": [{"name": "Delayed", "values": [on_track, delay]}], }, "type": "percentage", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 385d0f3a58..ee9161bee4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_party_account from erpnext.controllers.selling_controller import SellingController +from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( + validate_against_blanket_order, +) from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, ) @@ -52,6 +55,7 @@ class SalesOrder(SellingController): self.validate_warehouse() self.validate_drop_ship() self.validate_serial_no_based_delivery() + validate_against_blanket_order(self) validate_inter_company_party( self.doctype, self.customer, self.company, self.inter_company_order_reference ) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 6ea66a0237..45ad7d95a1 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -24,6 +24,7 @@ "so_required", "dn_required", "sales_update_frequency", + "over_order_allowance", "column_break_5", "allow_multiple_items", "allow_against_multiple_purchase_orders", @@ -179,6 +180,12 @@ "fieldname": "allow_sales_order_creation_for_expired_quotation", "fieldtype": "Check", "label": "Allow Sales Order Creation For Expired Quotation" + }, + { + "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", + "fieldname": "over_order_allowance", + "fieldtype": "Float", + "label": "Over Order Allowance (%)" } ], "icon": "fa fa-cog", @@ -186,7 +193,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-04 12:37:53.380857", + "modified": "2023-03-03 11:16:54.333615", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 07ee2890c4..fcdf245659 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -808,7 +808,7 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad return existing_address if out: - return min(out, key=lambda x: x[1])[0] # find min by sort_key + return max(out, key=lambda x: x[1])[0] # find max by sort_key else: return None diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index 29e056e34f..fd2fe300fa 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -11,6 +11,7 @@ from frappe.utils import random_string from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( get_charts_for_country, ) +from erpnext.setup.doctype.company.company import get_default_company_address test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"] test_dependencies = ["Fiscal Year"] @@ -132,6 +133,38 @@ class TestCompany(unittest.TestCase): self.assertTrue(lft >= min_lft) self.assertTrue(rgt <= max_rgt) + def test_primary_address(self): + company = "_Test Company" + + secondary = frappe.get_doc( + { + "address_title": "Non Primary", + "doctype": "Address", + "address_type": "Billing", + "address_line1": "Something", + "city": "Mumbai", + "state": "Maharashtra", + "country": "India", + "is_primary_address": 1, + "pincode": "400098", + "links": [ + { + "link_doctype": "Company", + "link_name": company, + } + ], + } + ) + secondary.insert() + self.addCleanup(secondary.delete) + + primary = frappe.copy_doc(secondary) + primary.is_primary_address = 1 + primary.insert() + self.addCleanup(primary.delete) + + self.assertEqual(get_default_company_address(company), primary.name) + def get_no_of_children(self, company): def get_no_of_children(companies, no_of_children): children = [] diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 1f7dddfb95..088958d1b2 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -155,7 +155,7 @@ def add_standard_navbar_items(): { "item_label": "Documentation", "item_type": "Route", - "route": "https://erpnext.com/docs/user/manual", + "route": "https://docs.erpnext.com/docs/v14/user/manual/en/introduction", "is_standard": 1, }, { diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 916ab2a05b..1763269193 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -636,7 +636,8 @@ "no_copy": 1, "options": "Sales Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "so_detail", @@ -837,7 +838,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-09 12:17:50.850142", + "modified": "2023-03-20 14:24:10.406746", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c06700a99a..05a37ee4c4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -377,7 +377,9 @@ class Item(Document): "" if item_barcode.barcode_type not in options else item_barcode.barcode_type ) if item_barcode.barcode_type: - barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper()) + barcode_type = convert_erpnext_to_barcodenumber( + item_barcode.barcode_type.upper(), item_barcode.barcode + ) if barcode_type in barcodenumber.barcodes(): if not barcodenumber.check_code(barcode_type, item_barcode.barcode): frappe.throw( @@ -982,20 +984,29 @@ class Item(Document): ) -def convert_erpnext_to_barcodenumber(erpnext_number): +def convert_erpnext_to_barcodenumber(erpnext_number, barcode): + if erpnext_number == "EAN": + ean_type = { + 8: "EAN8", + 13: "EAN13", + } + barcode_length = len(barcode) + if barcode_length in ean_type: + return ean_type[barcode_length] + + return erpnext_number + convert = { "UPC-A": "UPCA", "CODE-39": "CODE39", - "EAN": "EAN13", - "EAN-12": "EAN", - "EAN-8": "EAN8", "ISBN-10": "ISBN10", "ISBN-13": "ISBN13", } + if erpnext_number in convert: return convert[erpnext_number] - else: - return erpnext_number + + return erpnext_number def make_item_price(item, price_list_name, item_price): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 67ed90d4e7..0c6dc77635 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -581,8 +581,9 @@ class TestItem(FrappeTestCase): }, {"barcode": "72527273070", "barcode_type": "UPC-A"}, {"barcode": "123456", "barcode_type": "CODE-39"}, - {"barcode": "401268452363", "barcode_type": "EAN-12"}, - {"barcode": "90311017", "barcode_type": "EAN-8"}, + {"barcode": "401268452363", "barcode_type": "EAN"}, + {"barcode": "90311017", "barcode_type": "EAN"}, + {"barcode": "73513537", "barcode_type": "EAN"}, {"barcode": "0123456789012", "barcode_type": "GS1"}, {"barcode": "2211564566668", "barcode_type": "GTIN"}, {"barcode": "0256480249", "barcode_type": "ISBN"}, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index bf3b5ddc54..46d6e9e757 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -172,8 +172,8 @@ class PickList(Document): if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance: frappe.throw( _( - f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}." - ) + "You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}." + ).format(row.item_code, row.sales_order) ) @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 7f69397fce..36c875f308 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -658,6 +658,7 @@ class StockEntry(StockController): ) finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) + items = [] # Set basic rate for incoming items for d in self.get("items"): if d.s_warehouse or d.set_basic_rate_manually: @@ -665,12 +666,7 @@ class StockEntry(StockController): if d.allow_zero_valuation_rate: d.basic_rate = 0.0 - frappe.msgprint( - _( - "Row {0}: Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {1}" - ).format(d.idx, d.item_code), - alert=1, - ) + items.append(d.item_code) elif d.is_finished_item: if self.purpose == "Manufacture": @@ -697,6 +693,20 @@ class StockEntry(StockController): d.basic_rate = flt(d.basic_rate) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) + if items: + message = "" + + if len(items) > 1: + message = _( + "Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}" + ).format(", ".join(frappe.bold(item) for item in items)) + else: + message = _( + "Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}" + ).format(frappe.bold(items[0])) + + frappe.msgprint(message, alert=True) + def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): outgoing_items_cost = 0.0 for d in self.get("items"): diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 8367afd331..bace129213 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei Stock Levels,Niveaux du Stocks, Stock Liabilities,Passif du Stock, Stock Options,Options du Stock, -Stock Qty,Qté en Stock, +Stock Qty,Qté en unité de stock, Stock Received But Not Billed,Stock Reçus Mais Non Facturés, Stock Reports,Rapports de stock, Stock Summary,Résumé du Stock,