diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json index 7b56444e635..5f906b54aaa 100644 --- a/erpnext/accounts/doctype/account/account.json +++ b/erpnext/accounts/doctype/account/account.json @@ -128,7 +128,7 @@ "description": "Rate at which this tax is applied", "fieldname": "tax_rate", "fieldtype": "Float", - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "tax_rate", "oldfieldtype": "Currency" }, diff --git a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json index 05b284ae16f..544f4fd6640 100644 --- a/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json +++ b/erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json @@ -101,7 +101,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "oldfieldname": "rate", "oldfieldtype": "Currency" }, diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 42b1a54dea6..ce7e9436ad5 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -12,6 +12,7 @@ from frappe.utils import cint, flt from erpnext import get_default_cost_center from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount +from erpnext.accounts.party import get_party_account from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( get_amounts_not_reflected_in_system, get_entries, @@ -304,54 +305,56 @@ def create_payment_entry_bts( bank_transaction = frappe.db.get_values( "Bank Transaction", bank_transaction_name, - fieldname=["name", "unallocated_amount", "deposit", "bank_account"], + fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"], as_dict=True, )[0] - paid_amount = bank_transaction.unallocated_amount + payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay" - company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") - company = frappe.get_value("Account", company_account, "company") - payment_entry_dict = { - "company": company, - "payment_type": payment_type, - "reference_no": reference_number, - "reference_date": reference_date, - "party_type": party_type, - "party": party, - "posting_date": posting_date, - "paid_amount": paid_amount, - "received_amount": paid_amount, - } - payment_entry = frappe.new_doc("Payment Entry") + bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account") + company = frappe.get_cached_value("Account", bank_account, "company") + party_account = get_party_account(party_type, party, company) - payment_entry.update(payment_entry_dict) + bank_currency = bank_transaction.currency + party_currency = frappe.get_cached_value("Account", party_account, "account_currency") - if mode_of_payment: - payment_entry.mode_of_payment = mode_of_payment - if project: - payment_entry.project = project - if cost_center: - payment_entry.cost_center = cost_center - if payment_type == "Receive": - payment_entry.paid_to = company_account - else: - payment_entry.paid_from = company_account + exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date) - payment_entry.validate() + amt_in_bank_acc_currency = bank_transaction.unallocated_amount + amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate + + pe = frappe.new_doc("Payment Entry") + pe.payment_type = payment_type + pe.company = company + pe.reference_no = reference_number + pe.reference_date = reference_date + pe.party_type = party_type + pe.party = party + pe.posting_date = posting_date + pe.paid_from = party_account if payment_type == "Receive" else bank_account + pe.paid_to = party_account if payment_type == "Pay" else bank_account + pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency + pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency + pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency + pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency + pe.mode_of_payment = mode_of_payment + pe.project = project + pe.cost_center = cost_center + + pe.validate() if allow_edit: - return payment_entry + return pe - payment_entry.insert() + pe.insert() + pe.submit() - payment_entry.submit() vouchers = json.dumps( [ { "payment_doctype": "Payment Entry", - "payment_name": payment_entry.name, - "amount": paid_amount, + "payment_name": pe.name, + "amount": amt_in_bank_acc_currency, } ] ) @@ -480,8 +483,12 @@ def get_linked_payments( def subtract_allocations(gl_account, vouchers): "Look up & subtract any existing Bank Transaction allocations" copied = [] + + voucher_docs = [(voucher.get("doctype"), voucher.get("name")) for voucher in vouchers] + voucher_allocated_amounts = get_total_allocated_amount(voucher_docs) + for voucher in vouchers: - rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name")) + rows = voucher_allocated_amounts.get((voucher.get("doctype"), voucher.get("name"))) or [] filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows)) if amount := None if not filtered_row else filtered_row[0]["total"]: @@ -719,7 +726,7 @@ def get_pe_matching_query( (ref_rank + amount_rank + party_rank + 1).as_("rank"), ConstantColumn("Payment Entry").as_("doctype"), pe.name, - pe.paid_amount_after_tax.as_("paid_amount"), + pe.base_paid_amount_after_tax.as_("paid_amount"), pe.reference_no, pe.reference_date, pe.party, diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index c13dbe445f1..7e509896fec 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -154,10 +154,16 @@ class BankTransaction(Document): """ remaining_amount = self.unallocated_amount to_remove = [] + payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries] + pe_bt_allocations = get_total_allocated_amount(payment_entry_docs) + for payment_entry in self.payment_entries: if payment_entry.allocated_amount == 0.0: unallocated_amount, should_clear, latest_transaction = get_clearance_details( - self, payment_entry + self, + payment_entry, + pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry)) + or [], ) if 0.0 == unallocated_amount: @@ -232,7 +238,7 @@ def get_doctypes_for_bank_reconciliation(): return frappe.get_hooks("bank_reconciliation_doctypes") -def get_clearance_details(transaction, payment_entry): +def get_clearance_details(transaction, payment_entry, bt_allocations): """ There should only be one bank gle for a voucher. Could be none for a Bank Transaction. @@ -241,7 +247,6 @@ def get_clearance_details(transaction, payment_entry): """ gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry) - bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry) unallocated_amount = min( transaction.unallocated_amount, @@ -294,44 +299,52 @@ def get_related_bank_gl_entries(doctype, docname): ) -def get_total_allocated_amount(doctype, docname): +def get_total_allocated_amount(docs): """ Gets the sum of allocations for a voucher on each bank GL account along with the latest bank transaction name & date NOTE: query may also include just saved vouchers/payments but with zero allocated_amount """ + if not docs: + return {} + # nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql result = frappe.db.sql( """ - SELECT total, latest_name, latest_date, gl_account FROM ( + SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM ( SELECT ROW_NUMBER() OVER w AS rownum, - SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total, + SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total, FIRST_VALUE(bt.name) OVER w AS latest_name, FIRST_VALUE(bt.date) OVER w AS latest_date, - ba.account AS gl_account + ba.account AS gl_account, + btp.payment_document, + btp.payment_entry FROM `tabBank Transaction Payments` btp LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account WHERE - btp.payment_document = %(doctype)s - AND btp.payment_entry = %(docname)s + (btp.payment_document, btp.payment_entry) IN %(docs)s AND bt.docstatus = 1 - WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc) + WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC) ) temp WHERE rownum = 1 """, - dict(doctype=doctype, docname=docname), + dict(docs=docs), as_dict=True, ) + + payment_allocation_details = {} for row in result: # Why is this *sometimes* a byte string? if isinstance(row["latest_name"], bytes): row["latest_name"] = row["latest_name"].decode() row["latest_date"] = frappe.utils.getdate(row["latest_date"]) - return result + payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row) + + return payment_allocation_details def get_paid_amount(payment_entry, currency, gl_bank_account): diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index a7e7edb098d..d9d7807a561 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -13,7 +13,11 @@ import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency +from erpnext.accounts.party import ( + validate_account_party_type, + validate_party_frozen_disabled, + validate_party_gle_currency, +) from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency @@ -268,6 +272,7 @@ class GLEntry(Document): def validate_party(self): validate_party_frozen_disabled(self.party_type, self.party) + validate_account_party_type(self) def validate_currency(self): company_currency = erpnext.get_company_currency(self.company) diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py index 3edfd67b005..f6ed163bff5 100644 --- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py @@ -79,3 +79,48 @@ class TestGLEntry(unittest.TestCase): "SELECT current from tabSeries where name = %s", naming_series )[0][0] self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value) + + def test_validate_account_party_type(self): + jv = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 100, + "_Test Cost Center - _TC", + save=False, + submit=False, + ) + + for row in jv.accounts: + row.party_type = "Supplier" + break + + jv.save() + try: + jv.submit() + except Exception as e: + self.assertEqual( + str(e), + "Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC", + ) + + jv1 = make_journal_entry( + "_Test Account Cost for Goods Sold - _TC", + "_Test Bank - _TC", + 100, + "_Test Cost Center - _TC", + save=False, + submit=False, + ) + + for row in jv.accounts: + row.party_type = "Customer" + break + + jv1.save() + try: + jv1.submit() + except Exception as e: + self.assertEqual( + str(e), + "Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC", + ) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 8fd5f00b583..4a45007d50a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -27,6 +27,18 @@ frappe.ui.form.on("Payment Entry", { erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + // project excluded in setup_dimension_filters + frm.set_query("project", function (doc) { + let filters = { + company: doc.company, + }; + if (doc.party_type == "Customer") filters.customer = doc.party; + return { + query: "erpnext.controllers.queries.get_project_name", + filters, + }; + }); + if (frm.is_new()) { set_default_party_type(frm); } diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 312628d9f97..5883d4e2f1f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1479,7 +1479,7 @@ class TestPaymentEntry(FrappeTestCase): parent_account="Current Liabilities - _TC", account_name="Advances Paid", company=company, - account_type="Liability", + account_type="Payable", ) frappe.db.set_value( diff --git a/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html index 983f49563cd..63e88cf44c2 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html +++ b/erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html @@ -12,15 +12,15 @@ - {{ _('Grand Total') }} + {{ _("Grand Total") }} {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }} - {{ _('Net Total') }} + {{ _("Net Total") }} {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }} - {{ _('Total Quantity') }} + {{ _("Total Quantity") }} {{ data.total_quantity or '' }} @@ -44,7 +44,7 @@ {% for d in data.payment_reconciliation %} - {{ d.mode_of_payment }} + {{ _(d.mode_of_payment) }} {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }} {% endfor %} @@ -63,7 +63,7 @@ {{ _("Account") }} - {{ _("Rate") }} + {{ _("Tax Rate") }} {{ _("Amount") }} diff --git a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json index 42e7d0ef965..7e3e9c259f1 100644 --- a/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json +++ b/erpnext/accounts/doctype/pos_closing_entry_taxes/pos_closing_entry_taxes.json @@ -14,7 +14,7 @@ "fieldname": "rate", "fieldtype": "Percent", "in_list_view": 1, - "label": "Rate", + "label": "Tax Rate", "read_only": 1 }, { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index be429689b5a..dd8758e68a3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -399,6 +399,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. hide_fields(this.frm.doc); if (cint(this.frm.doc.is_paid)) { this.frm.set_value("allocate_advances_automatically", 0); + this.frm.set_value("payment_terms_template", ""); + this.frm.set_value("payment_schedule", []); if (!this.frm.doc.company) { this.frm.set_value("is_paid", 0); frappe.msgprint(__("Please specify Company to proceed")); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index dc2b44e7527..cb501c1ffbc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -507,7 +507,7 @@ class SalesInvoice(SellingController): frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) def validate_pos_paid_amount(self): - if len(self.payments) == 0 and self.is_pos: + if len(self.payments) == 0 and self.is_pos and flt(self.grand_total) > 0: frappe.throw(_("At least one mode of payment is required for POS invoice.")) def check_if_consolidated_invoice(self): diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 57eb84caaa4..439fc5639e5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -43,6 +43,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) +from erpnext.stock.get_item_details import get_item_tax_map from erpnext.stock.utils import get_incoming_rate, get_stock_balance @@ -2873,13 +2874,26 @@ class TestSalesInvoice(FrappeTestCase): item.save() sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True) + item_tax_map = get_item_tax_map( + company=sales_invoice.company, + item_tax_template=sales_invoice.items[0].item_tax_template, + ) + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") + self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map) # Apply discount sales_invoice.apply_discount_on = "Net Total" sales_invoice.discount_amount = 300 sales_invoice.save() + + item_tax_map = get_item_tax_map( + company=sales_invoice.company, + item_tax_template=sales_invoice.items[0].item_tax_template, + ) + self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") + self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map) @change_settings("Selling Settings", {"enable_discount_accounting": 1}) def test_sales_invoice_with_discount_accounting_enabled(self): diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 5be80872db8..3033b8ad087 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -759,6 +759,17 @@ def validate_party_frozen_disabled(party_type, party_name): frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True) +def validate_account_party_type(self): + if self.party_type and self.party: + account_type = frappe.get_cached_value("Account", self.account, "account_type") + if account_type and (account_type not in ["Receivable", "Payable"]): + frappe.throw( + _( + "Party Type and Party can only be set for Receivable / Payable account

" "{0}" + ).format(self.account) + ) + + def get_dashboard_info(party_type, party, loyalty_program=None): current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 604c0a6569d..f7a2e40b4ba 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -318,7 +318,7 @@ def get_columns(additional_table_columns, filters): "width": 100, }, { - "label": _("Rate"), + "label": _("Tax Rate"), "fieldname": "rate", "fieldtype": "Float", "options": "currency", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 144039b794f..51b4ed248ce 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -130,7 +130,9 @@ def get_fiscal_years( else: return ((fy.name, fy.year_start_date, fy.year_end_date),) - error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date)) + error_msg = _("""{0} {1} is not in any active Fiscal Year""").format( + _(label), formatdate(transaction_date) + ) if company: error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e499b5e0faa..2e0ba2c8218 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -19,6 +19,7 @@ from frappe.utils import ( ) import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.assets.doctype.asset.depreciation import ( get_comma_separated_links, @@ -887,6 +888,7 @@ def get_asset_naming_series(): @frappe.whitelist() def make_sales_invoice(asset, item_code, company, serial_no=None): + asset_doc = frappe.get_doc("Asset", asset) si = frappe.new_doc("Sales Invoice") si.company = company si.currency = frappe.get_cached_value("Company", company, "default_currency") @@ -903,6 +905,16 @@ def make_sales_invoice(asset, item_code, company, serial_no=None): "qty": 1, }, ) + + accounting_dimensions = get_dimensions(with_cost_center_and_project=True) + for dimension in accounting_dimensions[0]: + si.update( + { + dimension["fieldname"]: asset_doc.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) + si.set_missing_values() return si diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index 6d2034d1878..f583ce3e6c8 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -18,11 +18,12 @@ def execute(filters=None): columns = get_columns(filters) data = get_data(filters) - update_received_amount(data) if not data: return [], [], None, [] + update_received_amount(data) + data, chart_data = prepare_data(data, filters) return columns, data, None, chart_data @@ -103,6 +104,11 @@ def get_received_amount_data(data): pr = frappe.qb.DocType("Purchase Receipt") pr_item = frappe.qb.DocType("Purchase Receipt Item") + po_items = [row.name for row in data] + + if not po_items: + return frappe._dict() + query = ( frappe.qb.from_(pr) .inner_join(pr_item) @@ -111,12 +117,10 @@ def get_received_amount_data(data): pr_item.purchase_order_item, Sum(pr_item.base_amount).as_("received_qty_amount"), ) - .where((pr_item.parent == pr.name) & (pr.docstatus == 1)) + .where((pr.docstatus == 1) & (pr_item.purchase_order_item.isin(po_items))) .groupby(pr_item.purchase_order_item) ) - query = query.where(pr_item.purchase_order_item.isin([row.name for row in data])) - data = query.run() if not data: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 20b5885bfdc..f9c875477b9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -464,9 +464,16 @@ class AccountsController(TransactionBase): ) def validate_invoice_documents_schedule(self): - if self.is_return: + if ( + self.is_return + or (self.doctype == "Purchase Invoice" and self.is_paid) + or (self.doctype == "Sales Invoice" and self.is_pos) + or self.get("is_opening") == "Yes" + ): self.payment_terms_template = "" self.payment_schedule = [] + + if self.is_return: return self.validate_payment_schedule_dates() @@ -1219,7 +1226,7 @@ class AccountsController(TransactionBase): party_account = [] default_advance_account = None - if self.doctype == "Sales Invoice": + if self.doctype in ["Sales Invoice", "POS Invoice"]: party_type = "Customer" party = self.customer amount_field = "credit_in_account_currency" diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6020dce0761..8da22785b94 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -9,6 +9,7 @@ from frappe.utils import cint, flt, getdate from frappe.utils.data import nowtime import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.party import get_party_details from erpnext.buying.utils import update_last_purchase_rate, validate_for_items @@ -744,6 +745,7 @@ class BuyingController(SubcontractingController): def auto_make_assets(self, asset_items): items_data = get_asset_item_details(asset_items) messages = [] + accounting_dimensions = get_dimensions(with_cost_center_and_project=True) for d in self.items: if d.is_fixed_asset: @@ -755,11 +757,11 @@ class BuyingController(SubcontractingController): if item_data.get("asset_naming_series"): created_assets = [] if item_data.get("is_grouped_asset"): - asset = self.make_asset(d, is_grouped_asset=True) + asset = self.make_asset(d, accounting_dimensions, is_grouped_asset=True) created_assets.append(asset) else: for _qty in range(cint(d.qty)): - asset = self.make_asset(d) + asset = self.make_asset(d, accounting_dimensions) created_assets.append(asset) if len(created_assets) > 5: @@ -797,7 +799,7 @@ class BuyingController(SubcontractingController): for message in messages: frappe.msgprint(message, title="Success", indicator="green") - def make_asset(self, row, is_grouped_asset=False): + def make_asset(self, row, accounting_dimensions, is_grouped_asset=False): if not row.asset_location: frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code)) @@ -828,6 +830,13 @@ class BuyingController(SubcontractingController): "purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None, } ) + for dimension in accounting_dimensions[0]: + asset.update( + { + dimension["fieldname"]: self.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) asset.flags.ignore_validate = True asset.flags.ignore_mandatory = True diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 463cb859970..03852d3739a 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -271,10 +271,14 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): qb_filter_or_conditions = [] ifelse = CustomFunction("IF", ["condition", "then", "else"]) - if filters and filters.get("customer"): - qb_filter_and_conditions.append( - (proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == "" - ) + if filters: + if filters.get("customer"): + qb_filter_and_conditions.append( + (proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == "" + ) + + if filters.get("company"): + qb_filter_and_conditions.append(proj.company == filters.get("company")) qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled"])) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index e8aa6254d34..e1cd0a1c340 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -126,9 +126,13 @@ status_map = { "Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'", ], + [ + "Partially Received", + "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'", + ], [ "Partially Ordered", - "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1", + "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1 and self.material_request_type != 'Material Transfer'", ], [ "Manufactured", diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index bf5beab1a82..8fecb177295 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -18,7 +18,7 @@ from erpnext.controllers.accounts_controller import ( validate_inclusive_tax, validate_taxes_and_charges, ) -from erpnext.stock.get_item_details import _get_item_tax_template +from erpnext.stock.get_item_details import _get_item_tax_template, get_item_tax_map from erpnext.utilities.regional import temporary_flag @@ -70,6 +70,7 @@ class calculate_taxes_and_totals: self.validate_conversion_rate() self.calculate_item_values() self.validate_item_tax_template() + self.update_item_tax_map() self.initialize_taxes() self.determine_exclusive_rate() self.calculate_net_total() @@ -134,6 +135,14 @@ class calculate_taxes_and_totals: ) ) + def update_item_tax_map(self): + for item in self.doc.items: + item.item_tax_rate = get_item_tax_map( + company=self.doc.get("company"), + item_tax_template=item.item_tax_template, + as_json=True, + ) + def validate_conversion_rate(self): # validate conversion rate company_currency = erpnext.get_company_currency(self.doc.company) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json index a63fc4da69a..0e2e7273f10 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json @@ -13,6 +13,8 @@ "supplier", "supplier_name", "column_break_8", + "order_no", + "order_date", "from_date", "to_date", "company", @@ -129,15 +131,27 @@ "fieldname": "terms", "fieldtype": "Text Editor", "label": "Terms and Conditions Details" + }, + { + "fieldname": "order_no", + "fieldtype": "Data", + "label": "Order No" + }, + { + "depends_on": "eval:doc.order_no", + "fieldname": "order_date", + "fieldtype": "Date", + "label": "Order Date" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-29 00:30:30.621636", + "modified": "2024-12-05 15:44:21.520093", "modified_by": "Administrator", "module": "Manufacturing", "name": "Blanket Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 5ecf9d3a9d6..26fce935abb 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -31,6 +31,8 @@ class BlanketOrder(Document): from_date: DF.Date items: DF.Table[BlanketOrderItem] naming_series: DF.Literal["MFG-BLR-.YYYY.-"] + order_date: DF.Date | None + order_no: DF.Data | None supplier: DF.Link | None supplier_name: DF.Data | None tc_name: DF.Link | None diff --git a/erpnext/manufacturing/doctype/bom/bom_list.js b/erpnext/manufacturing/doctype/bom/bom_list.js index a26df545f85..1b6cba90dbc 100644 --- a/erpnext/manufacturing/doctype/bom/bom_list.js +++ b/erpnext/manufacturing/doctype/bom/bom_list.js @@ -2,13 +2,13 @@ frappe.listview_settings["BOM"] = { add_fields: ["is_active", "is_default", "total_cost", "has_variants"], get_indicator: function (doc) { if (doc.is_active && doc.has_variants) { - return [__("Template"), "orange", "has_variants,=,Yes"]; + return [__("Template"), "orange", "has_variants,=,1"]; } else if (doc.is_default) { - return [__("Default"), "green", "is_default,=,Yes"]; + return [__("Default"), "green", "is_default,=,1"]; } else if (doc.is_active) { - return [__("Active"), "blue", "is_active,=,Yes"]; + return [__("Active"), "blue", "is_active,=,1"]; } else if (!doc.is_active) { - return [__("Not active"), "gray", "is_active,=,No"]; + return [__("Not active"), "gray", "is_active,=,0"]; } }, }; diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index 0f5fa959dc5..6e66c9e539d 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -230,8 +230,8 @@ class ForecastingReport(ExponentialSmoothingForecast): "data": { "labels": labels, "datasets": [ - {"name": "Demand", "values": self.total_demand}, - {"name": "Forecast", "values": self.total_forecast}, + {"name": _("Demand"), "values": self.total_demand}, + {"name": _("Forecast"), "values": self.total_forecast}, ], }, "type": "line", diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index dc2b9ad62f3..e511612d3a3 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -106,7 +106,7 @@ def get_data(filters, columns): for label in labels: work = {} - work["Status"] = label + work["Status"] = _(label) for _dummy, end_date in ranges: period = get_period(end_date, filters) if periodic_data.get(label).get(period): diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py index 3e20f310ff9..f9c3154a63b 100644 --- a/erpnext/manufacturing/report/test_reports.py +++ b/erpnext/manufacturing/report/test_reports.py @@ -4,61 +4,67 @@ import frappe from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report -DEFAULT_FILTERS = { - "company": "_Test Company", - "from_date": "2010-01-01", - "to_date": "2030-01-01", - "warehouse": "_Test Warehouse - _TC", -} - - -REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ - ("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}), - ("BOM Operations Time", {}), - ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}), - ("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}), - ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}), - ("Downtime Analysis", {}), - ( - "Exponential Smoothing Forecasting", - { - "based_on_document": "Sales Order", - "based_on_field": "Qty", - "no_of_years": 3, - "periodicity": "Yearly", - "smoothing_constant": 0.3, - }, - ), - ("Job Card Summary", {"fiscal_year": "2021-2022"}), - ("Production Analytics", {"range": "Monthly"}), - ("Quality Inspection Summary", {}), - ("Process Loss Report", {}), - ("Work Order Stock Report", {}), - ("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}), -] - - -if frappe.db.a_row_exists("Production Plan"): - REPORT_FILTER_TEST_CASES.append( - ("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name}) - ) - -OPTIONAL_FILTERS = { - "warehouse": "_Test Warehouse - _TC", - "item": "_Test Item", - "item_group": "_Test Item Group", -} - class TestManufacturingReports(unittest.TestCase): + def setUp(self): + self.setup_default_filters() + + def tearDown(self): + frappe.db.rollback() + + def setup_default_filters(self): + self.last_bom = frappe.get_last_doc("BOM").name + self.DEFAULT_FILTERS = { + "company": "_Test Company", + "from_date": "2010-01-01", + "to_date": "2030-01-01", + "warehouse": "_Test Warehouse - _TC", + } + + self.REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ + ("BOM Explorer", {"bom": self.last_bom}), + ("BOM Operations Time", {}), + ("BOM Stock Calculated", {"bom": self.last_bom, "qty_to_make": 2}), + ("BOM Stock Report", {"bom": self.last_bom, "qty_to_produce": 2}), + ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}), + ("Downtime Analysis", {}), + ( + "Exponential Smoothing Forecasting", + { + "based_on_document": "Sales Order", + "based_on_field": "Qty", + "no_of_years": 3, + "periodicity": "Yearly", + "smoothing_constant": 0.3, + }, + ), + ("Job Card Summary", {"fiscal_year": "2021-2022"}), + ("Production Analytics", {"range": "Monthly"}), + ("Quality Inspection Summary", {}), + ("Process Loss Report", {}), + ("Work Order Stock Report", {}), + ("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}), + ] + + if frappe.db.a_row_exists("Production Plan"): + self.REPORT_FILTER_TEST_CASES.append( + ("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name}) + ) + + self.OPTIONAL_FILTERS = { + "warehouse": "_Test Warehouse - _TC", + "item": "_Test Item", + "item_group": "_Test Item Group", + } + def test_execute_all_manufacturing_reports(self): """Test that all script report in manufacturing modules are executable with supported filters""" - for report, filter in REPORT_FILTER_TEST_CASES: + for report, filter in self.REPORT_FILTER_TEST_CASES: with self.subTest(report=report): execute_script_report( report_name=report, module="Manufacturing", filters=filter, - default_filters=DEFAULT_FILTERS, - optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, + default_filters=self.DEFAULT_FILTERS, + optional_filters=self.OPTIONAL_FILTERS if filter.get("_optional") else None, ) diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 21aa70fe7ae..b1019f67ca9 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -147,10 +147,10 @@ class BOMConfigurator { if (!node.expanded) { view.tree.load_children(node, true); $(node.parent[0]).find(".tree-children").show(); - node.$toolbar.find(".expand-all-btn").html("Collapse All"); + node.$toolbar.find(".expand-all-btn").html(__("Collapse All")); } else { node.$tree_link.trigger("click"); - node.$toolbar.find(".expand-all-btn").html("Expand All"); + node.$toolbar.find(".expand-all-btn").html(__("Expand All")); } }, condition: function (node) { @@ -190,10 +190,10 @@ class BOMConfigurator { if (!node.expanded) { view.tree.load_children(node, true); $(node.parent[0]).find(".tree-children").show(); - node.$toolbar.find(".expand-all-btn").html("Collapse All"); + node.$toolbar.find(".expand-all-btn").html(__("Collapse All")); } else { node.$tree_link.trigger("click"); - node.$toolbar.find(".expand-all-btn").html("Expand All"); + node.$toolbar.find(".expand-all-btn").html(__("Expand All")); } }, condition: function (node) { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 202efe157f0..af61d5f0258 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -25,6 +25,14 @@ erpnext.buying = { }; }); + this.frm.set_query("project", function (doc) { + return { + filters: { + company: doc.company, + }, + }; + }); + if (this.frm.doc.__islocal && frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) { @@ -145,6 +153,18 @@ erpnext.buying = { }); } + company(){ + if(!frappe.meta.has_field(this.frm.doc.doctype, "billing_address")) return; + + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: { name: this.frm.doc.company, existing_address:this.frm.doc.billing_address }, + callback: (r) => { + this.frm.set_value("billing_address", r.message || ""); + }, + }); + } + supplier_address() { erpnext.utils.get_address_display(this.frm); erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address"); diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 18cddd7f7a1..db866bd3b76 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -813,6 +813,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } validate() { + this.apply_pricing_rule() this.calculate_taxes_and_totals(false); } @@ -959,7 +960,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier); if (!is_drop_ship) { - console.log('get_shipping_address'); erpnext.utils.get_shipping_address(this.frm, function() { set_party_account(set_pricing); }); @@ -975,6 +975,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } transaction_date() { + this.apply_pricing_rule() if (this.frm.doc.transaction_date) { this.frm.transaction_date = this.frm.doc.transaction_date; frappe.ui.form.trigger(this.frm.doc.doctype, "currency"); @@ -983,6 +984,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe posting_date() { var me = this; + me.apply_pricing_rule() if (this.frm.doc.posting_date) { this.frm.posting_date = this.frm.doc.posting_date; @@ -2310,6 +2312,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe fieldname: "batch_no", label: __("Batch No"), hidden: true + }, + { + fieldtype: "Data", + fieldname: "child_row_reference", + label: __("Child Row Reference"), + hidden: true } ] } @@ -2353,14 +2361,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (this.has_inspection_required(item)) { let dialog_items = dialog.fields_dict.items; dialog_items.df.data.push({ - "docname": item.name, "item_code": item.item_code, "item_name": item.item_name, "qty": item.qty, "description": item.description, "serial_no": item.serial_no, "batch_no": item.batch_no, - "sample_size": item.sample_quantity + "sample_size": item.sample_quantity, + "child_row_reference": item.name, }); dialog_items.grid.refresh(); } diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 32cb2bc0525..dc7e992f654 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -285,6 +285,7 @@ erpnext.PointOfSale.Controller = class { edit_cart: () => this.payment.edit_cart(), customer_details_updated: (details) => { + this.item_selector.load_items_data(); this.customer_details = details; // will add/remove LP payment method this.payment.render_loyalty_points_payment_mode(); 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 6342b237f6e..325f7b258a9 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -390,6 +390,14 @@ erpnext.PointOfSale.ItemCart = class { input_class: "input-xs", onchange: function () { this.value = flt(this.value); + if (this.value > 100) { + frappe.msgprint({ + title: __("Invalid Discount"), + indicator: "red", + message: __("Discount cannot be greater than 100%."), + }); + this.value = 0; + } frappe.model.set_value( frm.doc.doctype, frm.doc.name, diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index ad4b4cd15be..2c93a0d546b 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -315,8 +315,12 @@ erpnext.PointOfSale.ItemDetails = class { frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { const field_control = this[`${fieldname}_control`]; const item_row_is_being_edited = this.compare_with_current_item(item_row); - - if (item_row_is_being_edited && field_control && field_control.get_value() !== value) { + if ( + item_row_is_being_edited && + field_control && + field_control.get_value() !== value && + value == item_row[fieldname] + ) { field_control.set_value(value); cur_pos.update_cart_html(item_row); } diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index bea1918fa20..92349d27aca 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -235,7 +235,7 @@ erpnext.PointOfSale.Payment = class { frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => { // for setting correct amount after loyalty points are redeemed const default_mop = locals[cdt][cdn]; - const mode = default_mop.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + const mode = this.sanitize_mode_of_payment(default_mop.mode_of_payment); if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) { this[`${mode}_control`].set_value(default_mop.amount); } @@ -388,7 +388,7 @@ erpnext.PointOfSale.Payment = class { this.$payment_modes.html( `${payments .map((p, i) => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + const mode = this.sanitize_mode_of_payment(p.mode_of_payment); const payment_type = p.type; const margin = i % 2 === 0 ? "pr-2" : "pl-2"; const amount = p.amount > 0 ? format_currency(p.amount, currency) : ""; @@ -407,7 +407,7 @@ erpnext.PointOfSale.Payment = class { ); payments.forEach((p) => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + const mode = this.sanitize_mode_of_payment(p.mode_of_payment); const me = this; this[`${mode}_control`] = frappe.ui.form.make_control({ df: { @@ -442,7 +442,7 @@ erpnext.PointOfSale.Payment = class { const doc = this.events.get_frm().doc; const payments = doc.payments; payments.forEach((p) => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + const mode = this.sanitize_mode_of_payment(p.mode_of_payment); if (p.default) { setTimeout(() => { this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click(); @@ -612,4 +612,12 @@ erpnext.PointOfSale.Payment = class { toggle_component(show) { show ? this.$component.css("display", "flex") : this.$component.css("display", "none"); } + + sanitize_mode_of_payment(mode_of_payment) { + return mode_of_payment + .replace(/ +/g, "_") + .replace(/[^\p{L}\p{N}_-]/gu, "") + .replace(/^[^_a-zA-Z\p{L}]+/u, "") + .toLowerCase(); + } }; diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 6561f386c55..f8cd61d50ea 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -292,7 +292,7 @@ def get_or_create_tax_group(company_name, root_type): tax_group_account.flags.ignore_links = True tax_group_account.flags.ignore_validate = True - tax_group_account.insert(ignore_permissions=True) + tax_group_account.insert(ignore_permissions=True, ignore_if_duplicate=True) tax_group_name = tax_group_account.name diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index b3f4c2dead4..7611d751fdd 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -181,6 +181,9 @@ class DeprecatedBatchNoValuation: stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty self.stock_value_change += stock_value_change + self.non_batchwise_balance_value[batch_no] -= stock_value_change + self.non_batchwise_balance_qty[batch_no] -= ledger.qty + frappe.db.set_value( "Serial and Batch Entry", ledger.name, @@ -220,7 +223,6 @@ class DeprecatedBatchNoValuation: .select( sle.batch_no, Sum(sle.actual_qty).as_("batch_qty"), - Sum(sle.stock_value_difference).as_("batch_value"), ) .where( (sle.item_code == self.sle.item_code) @@ -237,11 +239,59 @@ class DeprecatedBatchNoValuation: if self.sle.name: query = query.where(sle.name != self.sle.name) - for d in query.run(as_dict=True): - self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value) - self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty) + batch_data = query.run(as_dict=True) + for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) + last_sle = self.get_last_sle_for_non_batch() + for d in batch_data: + self.non_batchwise_balance_value[d.batch_no] += flt(last_sle.stock_value) + self.non_batchwise_balance_qty[d.batch_no] += flt(last_sle.qty_after_transaction) + + def get_last_sle_for_non_batch(self): + from erpnext.stock.utils import get_combine_datetime + + sle = frappe.qb.DocType("Stock Ledger Entry") + batch = frappe.qb.DocType("Batch") + + posting_datetime = get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + if not self.sle.creation: + posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) + + timestamp_condition = sle.posting_datetime < posting_datetime + + if self.sle.creation: + timestamp_condition |= (sle.posting_datetime == posting_datetime) & ( + sle.creation < self.sle.creation + ) + + query = ( + frappe.qb.from_(sle) + .inner_join(batch) + .on(sle.batch_no == batch.name) + .select( + sle.stock_value, + sle.qty_after_transaction, + ) + .where( + (sle.item_code == self.sle.item_code) + & (sle.warehouse == self.sle.warehouse) + & (sle.batch_no.isnotnull()) + & (batch.use_batchwise_valuation == 0) + & (sle.is_cancelled == 0) + ) + .where(timestamp_condition) + .orderby(sle.posting_datetime, order=Order.desc) + .orderby(sle.creation, order=Order.desc) + .limit(1) + ) + + if self.sle.name: + query = query.where(sle.name != self.sle.name) + + data = query.run(as_dict=True) + return data[0] if data else {} + @deprecated def set_balance_value_from_bundle(self) -> None: bundle = frappe.qb.DocType("Serial and Batch Bundle") 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 8aa49f7cfd8..f6510c04fe9 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -65,7 +65,7 @@ class ClosingStockBalance(Document): & ( (table.from_date.between(self.from_date, self.to_date)) | (table.to_date.between(self.from_date, self.to_date)) - | ((table.from_date >= self.from_date) & (table.to_date >= self.to_date)) + | ((self.from_date >= table.from_date) & (table.from_date >= self.to_date)) ) ) ) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 661605bdf5f..524c7331bc7 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -237,8 +237,13 @@ class InventoryDimension(Document): custom_fields["Stock Ledger Entry"] = dimension_field filter_custom_fields = {} + ignore_doctypes = ["Serial and Batch Bundle", "Serial and Batch Entry", "Pick List Item"] + if custom_fields: for doctype, fields in custom_fields.items(): + if doctype in ignore_doctypes: + continue + if isinstance(fields, dict): fields = [fields] diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 09c01a0ee88..83b63e225b3 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -322,7 +322,7 @@ frappe.ui.form.on("Material Request", { default: 1, }, ], - primary_action_label: "Get Items", + primary_action_label: __("Get Items"), primary_action(values) { if (!values) return; values["company"] = frm.doc.company; diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 23d289170db..f59b60a3f51 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -766,6 +766,7 @@ def raise_work_orders(material_request): ) wo_order.set_work_order_operations() + wo_order.flags.ignore_validate = True wo_order.flags.ignore_mandatory = True wo_order.save() diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index ee5c9e7b86c..57332aa7730 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -14,6 +14,12 @@ frappe.listview_settings["Material Request"] = { } } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; + } else if ( + doc.docstatus == 1 && + flt(doc.per_ordered, precision) < 100 && + doc.material_request_type == "Material Transfer" + ) { + return [__("Partially Received"), "yellow", "per_ordered,<,100"]; } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) < 100) { return [__("Partially ordered"), "yellow", "per_ordered,<,100"]; } else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 100) { diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 4d48d50c9f0..b0ea7964117 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -17,6 +17,7 @@ from erpnext.stock.doctype.material_request.material_request import ( make_supplier_quotation, raise_work_orders, ) +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestMaterialRequest(FrappeTestCase): @@ -59,6 +60,43 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) + def test_partial_make_stock_entry(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry as _make_stock_entry + + mr = frappe.copy_doc(test_records[0]).insert() + + source_wh = create_warehouse( + warehouse_name="_Test Source Warehouse", + properties={"parent_warehouse": "All Warehouses - _TC"}, + company="_Test Company", + ) + + mr = frappe.get_doc("Material Request", mr.name) + mr.material_request_type = "Material Transfer" + + for row in mr.items: + _make_stock_entry( + item_code=row.item_code, + qty=10, + to_warehouse=source_wh, + company="_Test Company", + rate=100, + ) + + row.from_warehouse = source_wh + row.qty = 10 + + mr.save() + mr.submit() + + se = make_stock_entry(mr.name) + se.get("items")[0].qty = 5 + se.insert() + se.submit() + + mr.reload() + self.assertEqual(mr.status, "Partially Received") + def test_in_transit_make_stock_entry(self): mr = frappe.copy_doc(test_records[0]).insert() diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index 914a9f3c21f..56017c4fee2 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -15,6 +15,7 @@ "inspection_type", "reference_type", "reference_name", + "child_row_reference", "section_break_7", "item_code", "item_serial_no", @@ -238,6 +239,15 @@ "fieldname": "manual_inspection", "fieldtype": "Check", "label": "Manual Inspection" + }, + { + "fieldname": "child_row_reference", + "fieldtype": "Data", + "hidden": 1, + "label": "Child Row Reference", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-search", @@ -245,7 +255,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-23 11:56:50.282878", + "modified": "2024-12-30 19:08:16.611192", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", @@ -272,4 +282,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 6890256dc04..d3b5e65ea9f 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -29,6 +29,7 @@ class QualityInspection(Document): amended_from: DF.Link | None batch_no: DF.Link | None bom_no: DF.Link | None + child_row_reference: DF.Data | None description: DF.SmallText | None inspected_by: DF.Link inspection_type: DF.Literal["", "Incoming", "Outgoing", "In Process"] @@ -74,6 +75,64 @@ class QualityInspection(Document): self.inspect_and_set_status() self.validate_inspection_required() + self.set_child_row_reference() + + def set_child_row_reference(self): + if self.child_row_reference: + return + + if not (self.reference_type and self.reference_name): + return + + doctype = self.reference_type + " Item" + if self.reference_type == "Stock Entry": + doctype = "Stock Entry Detail" + + child_row_references = frappe.get_all( + doctype, + filters={"parent": self.reference_name, "item_code": self.item_code}, + pluck="name", + ) + + if not child_row_references: + return + + if len(child_row_references) == 1: + self.child_row_reference = child_row_references[0] + else: + self.distribute_child_row_reference(child_row_references) + + def distribute_child_row_reference(self, child_row_references): + quality_inspections = frappe.get_all( + "Quality Inspection", + filters={ + "reference_name": self.reference_name, + "item_code": self.item_code, + "docstatus": ("<", 2), + }, + fields=["name", "child_row_reference", "docstatus"], + order_by="child_row_reference desc", + ) + + for row in quality_inspections: + if not child_row_references: + break + + if row.child_row_reference and row.child_row_reference in child_row_references: + child_row_references.remove(row.child_row_reference) + continue + + if row.docstatus == 1: + continue + + if row.name == self.name: + self.child_row_reference = child_row_references[0] + else: + frappe.db.set_value( + "Quality Inspection", row.name, "child_row_reference", child_row_references[0] + ) + + child_row_references.remove(child_row_references[0]) def validate_inspection_required(self): if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value( @@ -157,35 +216,38 @@ class QualityInspection(Document): ) else: - args = [quality_inspection, self.modified, self.reference_name, self.item_code] doctype = self.reference_type + " Item" if self.reference_type == "Stock Entry": doctype = "Stock Entry Detail" - if self.reference_type and self.reference_name: - conditions = "" + if doctype and self.reference_name: + child_doc = frappe.qb.DocType(doctype) + + query = ( + frappe.qb.update(child_doc) + .set(child_doc.quality_inspection, quality_inspection) + .where( + (child_doc.parent == self.reference_name) & (child_doc.item_code == self.item_code) + ) + ) + if self.batch_no and self.docstatus == 1: - conditions += " and t1.batch_no = %s" - args.append(self.batch_no) + query = query.where(child_doc.batch_no == self.batch_no) if self.docstatus == 2: # if cancel, then remove qi link wherever same name - conditions += " and t1.quality_inspection = %s" - args.append(self.name) + query = query.where(child_doc.quality_inspection == self.name) - frappe.db.sql( - f""" - UPDATE - `tab{doctype}` t1, `tab{self.reference_type}` t2 - SET - t1.quality_inspection = %s, t2.modified = %s - WHERE - t1.parent = %s - and t1.item_code = %s - and t1.parent = t2.name - {conditions} - """, - args, + if self.child_row_reference: + query = query.where(child_doc.name == self.child_row_reference) + + query.run() + + frappe.db.set_value( + self.reference_type, + self.reference_name, + "modified", + self.modified, ) def inspect_and_set_status(self): diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 647082baa68..45a474df2b7 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -166,7 +166,7 @@ class TestSerialandBatchBundle(FrappeTestCase): for qty, valuation in {10: 100, 20: 200}.items(): stock_queue.append([qty, valuation]) qty_after_transaction += qty - balance_value += qty_after_transaction * valuation + balance_value += qty * valuation doc = frappe.get_doc( { @@ -177,6 +177,7 @@ class TestSerialandBatchBundle(FrappeTestCase): "incoming_rate": valuation, "qty_after_transaction": qty_after_transaction, "stock_value_difference": valuation * qty, + "stock_value": balance_value, "balance_value": balance_value, "valuation_rate": balance_value / qty_after_transaction, "actual_qty": qty, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index febc814b978..78f69b1ce62 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3289,8 +3289,10 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1}) elif row.batches_to_be_consume: + precision = frappe.get_precision("Serial and Batch Entry", "qty") doc.has_batch_no = 1 for batch_no, qty in row.batches_to_be_consume.items(): + qty = flt(qty, precision) doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1}) if not doc.entries: diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d5d492a2c93..17a8fe2cb6a 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -592,7 +592,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date - validation_date = args.get("bill_date") or args.get("transaction_date") + + validation_date = ( + args.get("bill_date") or args.get("posting_date") or args.get("transaction_date") + ) if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax): taxes_with_validity.append(tax) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 37cd43ac1f6..300f7a774eb 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -775,6 +775,9 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False postprocess=post_process, ) + if not target_doc.get("items"): + add_po_items_to_pr(source_doc, target_doc) + if (save or submit) and frappe.has_permission(target_doc.doctype, "create"): target_doc.save() @@ -794,3 +797,29 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False ) return target_doc + + +def add_po_items_to_pr(scr_doc, target_doc): + fg_items = {(item.item_code, item.purchase_order): item.qty for item in scr_doc.items} + + for (item_code, po_name), fg_qty in fg_items.items(): + po_doc = frappe.get_doc("Purchase Order", po_name) + for item in po_doc.items: + if item.fg_item != item_code: + continue + + qty = (item.stock_qty - item.received_qty) * fg_qty / item.fg_item_qty + if qty: + target_doc.append( + "items", + { + "item_code": item.item_code, + "item_name": item.item_name, + "description": item.description, + "qty": qty, + "rate": item.rate, + "warehouse": item.warehouse, + "purchase_order": item.parent, + "purchase_order_item": item.name, + }, + ) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index e0fa7923ef9..b5d190f7736 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -1137,6 +1137,80 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60) + @change_settings("Buying Settings", {"auto_create_purchase_receipt": 1}) + def test_auto_create_purchase_receipt_with_no_reference_of_po_item(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + fg_item = "Subcontracted Item SA1" + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 5, + }, + ] + + po = create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse="_Test Warehouse 1 - _TC", + do_not_submit=True, + ) + po.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Purchase Taxes and Charges", + "rate": 10, + }, + ) + po.save() + po.submit() + + sco = get_subcontracting_order(po_name=po.name) + for row in sco.items: + row.db_set("purchase_order_item", None) + + sco.reload() + + for row in sco.items: + self.assertFalse(row.purchase_order_item) + + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr = make_subcontracting_receipt(sco.name) + for row in scr.items: + self.assertFalse(row.purchase_order_item) + + scr.items[0].qty = 3 + scr.save() + scr.submit() + + pr_details = frappe.get_all( + "Purchase Receipt", + filters={"subcontracting_receipt": scr.name}, + fields=["name", "total_taxes_and_charges"], + ) + + self.assertTrue(pr_details) + + pr_qty = frappe.db.get_value("Purchase Receipt Item", {"parent": pr_details[0]["name"]}, "qty") + self.assertEqual(pr_qty, 6) + + self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60) + def test_use_serial_batch_fields_for_subcontracting_receipt(self): fg_item = make_item( "Test Subcontracted Item With Batch No", diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html index 0072dc280c7..11e0adaa2ee 100644 --- a/erpnext/templates/includes/footer/footer_extension.html +++ b/erpnext/templates/includes/footer/footer_extension.html @@ -17,7 +17,7 @@ frappe.ready(function() { if($("#footer-subscribe-email").val() && validate_email($("#footer-subscribe-email").val())) { $("#footer-subscribe-email").attr('disabled', true); - $("#footer-subscribe-button").html("Sending...") + $("#footer-subscribe-button").html(__("Sending...")) .attr("disabled", true); erpnext.subscribe_to_newsletter({ email: $("#footer-subscribe-email").val(), diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html index d7b9620fa0a..42f46ac8d12 100644 --- a/erpnext/templates/includes/order/order_taxes.html +++ b/erpnext/templates/includes/order/order_taxes.html @@ -12,14 +12,14 @@ {% endif %} {% for d in doc.taxes %} - {% if d.base_tax_amount %} + {% if d.tax_amount %}
{{ d.description }}
- {{ d.get_formatted("base_tax_amount") }} + {{ d.get_formatted("tax_amount") }}
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 388feb9eba9..ade66dd481f 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -40,7 +40,7 @@

- {{ _("Pay") }} {{doc.get_formatted("grand_total") }} + {{ _("Pay", null, "Amount") }} {{ pay_amount }}