diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index d03ebed353e..7959f163871 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -147,8 +147,8 @@ class AccountsSettings(Document): if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template: frappe.throw( _("You cannot enable both the settings '{0}' and '{1}'.").format( - frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")), - frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")), + frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))), + frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))), ), title=_("Auto Tax Settings Error"), ) diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 6d9a6f51468..6d256382042 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -6,7 +6,11 @@ import unittest import frappe from frappe.utils import now_datetime, nowdate -from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense +from erpnext.accounts.doctype.budget.budget import ( + BudgetError, + get_accumulated_monthly_budget, + get_actual_expense, +) from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase): frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year) + accumulated_limit = get_accumulated_monthly_budget( + budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount + ) + mr = frappe.get_doc( { "doctype": "Material Request", @@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase): "uom": "_Test UOM", "warehouse": "_Test Warehouse - _TC", "schedule_date": nowdate(), - "rate": 100000, + "rate": accumulated_limit + 1, "expense_account": "_Test Account Cost for Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", } diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 58c0f23e05e..d05e9e3b2d1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, get_account_currency, + get_advance_payment_doctypes, get_balance_on, get_stock_accounts, get_stock_and_account_balance, @@ -146,8 +147,8 @@ class JournalEntry(AccountsController): if self.docstatus == 0: self.apply_tax_withholding() - - self.title = self.get_title() + if self.is_new() or not self.title: + self.title = self.get_title() def validate_advance_accounts(self): journal_accounts = set([x.account for x in self.accounts]) @@ -238,9 +239,10 @@ class JournalEntry(AccountsController): def update_advance_paid(self): advance_paid = frappe._dict() + advance_payment_doctypes = get_advance_payment_doctypes() for d in self.get("accounts"): if d.is_advance: - if d.reference_type in frappe.get_hooks("advance_payment_doctypes"): + if d.reference_type in advance_payment_doctypes: advance_paid.setdefault(d.reference_type, []).append(d.reference_name) for voucher_type, order_list in advance_paid.items(): @@ -1042,7 +1044,9 @@ class JournalEntry(AccountsController): def set_print_format_fields(self): bank_amount = party_amount = total_amount = 0.0 - currency = bank_account_currency = party_account_currency = pay_to_recd_from = None + currency = ( + bank_account_currency + ) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None party_type = None for d in self.get("accounts"): if d.party_type in ["Customer", "Supplier"] and d.party: diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index f96145b65d1..de4305f86e0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -46,8 +46,10 @@ from erpnext.accounts.party import ( from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, get_account_currency, + get_advance_payment_doctypes, get_balance_on, get_outstanding_invoices, + get_reconciliation_effect_date, ) from erpnext.controllers.accounts_controller import ( AccountsController, @@ -568,7 +570,7 @@ class PaymentEntry(AccountsController): def validate_mandatory(self): for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"): if not self.get(field): - frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field))) + frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field)))) def validate_reference_documents(self): valid_reference_doctypes = self.get_valid_reference_doctypes() @@ -1028,7 +1030,7 @@ class PaymentEntry(AccountsController): def calculate_base_allocated_amount_for_reference(self, d) -> float: base_allocated_amount = 0 - if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): + if d.reference_doctype in get_advance_payment_doctypes(): # When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type. # This is so there are no Exchange Gain/Loss generated for such doctypes @@ -1308,8 +1310,7 @@ class PaymentEntry(AccountsController): if not self.party_account: return - advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes") - + advance_payment_doctypes = get_advance_payment_doctypes() if self.payment_type == "Receive": against_account = self.paid_to else: @@ -1492,23 +1493,7 @@ class PaymentEntry(AccountsController): else: # For backwards compatibility # Supporting reposting on payment entries reconciled before select field introduction - reconciliation_takes_effect_on = frappe.get_cached_value( - "Company", self.company, "reconciliation_takes_effect_on" - ) - if reconciliation_takes_effect_on == "Advance Payment Date": - posting_date = self.posting_date - elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": - date_field = "posting_date" - if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: - date_field = "transaction_date" - posting_date = frappe.db.get_value( - invoice.reference_doctype, invoice.reference_name, date_field - ) - - if getdate(posting_date) < getdate(self.posting_date): - posting_date = self.posting_date - elif reconciliation_takes_effect_on == "Reconciliation Date": - posting_date = nowdate() + posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date) frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) dr_or_cr, account = self.get_dr_and_account_for_advances(invoice) @@ -1699,12 +1684,15 @@ class PaymentEntry(AccountsController): return flt(gl_dict.get(field, 0) / (conversion_rate or 1)) def update_advance_paid(self): - if self.payment_type in ("Receive", "Pay") and self.party: - for d in self.get("references"): - if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): - frappe.get_doc( - d.reference_doctype, d.reference_name, for_update=True - ).set_total_advance_paid() + if self.payment_type not in ("Receive", "Pay") or not self.party: + return + + advance_payment_doctypes = get_advance_payment_doctypes() + for d in self.get("references"): + if d.allocated_amount and d.reference_doctype in advance_payment_doctypes: + frappe.get_doc( + d.reference_doctype, d.reference_name, for_update=True + ).set_total_advance_paid() def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 27001e9ab14..c77870d0886 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -589,7 +589,7 @@ class PaymentReconciliation(Document): def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: if not self.get(fieldname): - frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname))) + frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname)))) def validate_entries(self): if not self.get("invoices"): diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 344248b57b6..8bb120f1ab5 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -829,8 +829,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): if not references: return - precision = references[0].precision("allocated_amount") - + precision = frappe.get_precision("Payment Entry Reference", "allocated_amount") referenced_payment_requests = frappe.get_all( "Payment Request", filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]}, diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index c9628bda433..f38c58d6d32 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -630,29 +630,58 @@ class TestPaymentRequest(FrappeTestCase): pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1) self.assertEqual(pr.grand_total, si.outstanding_amount) + def test_partial_paid_invoice_with_submitted_payment_entry(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) + pi.save() + pi.submit() -def test_partial_paid_invoice_with_submitted_payment_entry(self): - pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) - pi.save() - pi.submit() + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0001" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + pe.cancel() - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") - pe.reference_no = "PURINV0001" - pe.reference_date = frappe.utils.nowdate() - pe.paid_amount = 2500 - pe.references[0].allocated_amount = 2500 - pe.save() - pe.submit() - pe.cancel() + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0002" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") - pe.reference_no = "PURINV0002" - pe.reference_date = frappe.utils.nowdate() - pe.paid_amount = 2500 - pe.references[0].allocated_amount = 2500 - pe.save() - pe.submit() + pi.load_from_db() + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + self.assertEqual(pr.grand_total, pi.outstanding_amount) - pi.load_from_db() - pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) - self.assertEqual(pr.grand_total, pi.outstanding_amount) + def test_payment_request_on_unreconcile(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=500) + pi.submit() + + pr = make_payment_request( + dt=pi.doctype, + dn=pi.name, + mute_email=1, + submit_doc=True, + return_doc=True, + ) + self.assertEqual(pr.grand_total, pi.outstanding_amount) + + pe = pr.create_payment_entry() + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": pe.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + unreconcile.submit() + + pi.load_from_db() + pr.load_from_db() + + self.assertEqual(pr.grand_total, pi.outstanding_amount) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json index a0594556474..7d9b90f1b65 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json @@ -5,6 +5,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "company", "posting_date", "posting_time", "merge_invoices_based_on", @@ -113,12 +114,22 @@ "label": "Posting Time", "no_copy": 1, "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "print_hide": 1, + "remember_last_selected_value": 1, + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-08-01 11:36:42.456429", + "modified": "2025-07-02 17:08:04.747202", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Merge Log", @@ -179,8 +190,9 @@ "write": 1 } ], - "sort_field": "modified", + "row_format": "Dynamic", + "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 3d6d03f7380..fedc6a7772d 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -28,11 +28,10 @@ class POSInvoiceMergeLog(Document): if TYPE_CHECKING: from frappe.types import DF - from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import ( - POSInvoiceReference, - ) + from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference amended_from: DF.Link | None + company: DF.Link consolidated_credit_note: DF.Link | None consolidated_invoice: DF.Link | None customer: DF.Link @@ -584,6 +583,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): merge_log.posting_time = ( get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() ) + merge_log.company = closing_entry.get("company") if closing_entry else None merge_log.customer = customer merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None merge_log.set("pos_invoices", _invoices) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index f0481ad63f6..21e5ad18d9b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -169,7 +169,7 @@ class PricingRule(Document): tocheck = frappe.scrub(self.get("applicable_for", "")) if tocheck and not self.get(tocheck): - throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError) + throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError) if self.apply_rule_on_other: o_field = "other_" + frappe.scrub(self.apply_rule_on_other) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 3ad3d45ee47..123c17f9b75 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -205,6 +205,56 @@ class TestPricingRule(FrappeTestCase): details = get_item_details(args) self.assertEqual(details.get("discount_percentage"), 10) + def test_unset_group_condition(self): + """ + If args are not set for group condition, then pricing rule should not be applied. + """ + from erpnext.stock.get_item_details import get_item_details + + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "items": [{"item_code": "_Test Item"}], + "currency": "USD", + "selling": 1, + "rate_or_discount": "Discount Percentage", + "rate": 0, + "discount_percentage": 10, + "applicable_for": "Territory", + "territory": "All Territories", + "company": "_Test Company", + } + frappe.get_doc(test_record.copy()).insert() + args = frappe._dict( + { + "item_code": "_Test Item", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "name": None, + } + ) + + # without territory in customer + customer = frappe.get_doc("Customer", "_Test Customer") + territory = customer.territory + + customer.territory = None + customer.save() + + details = get_item_details(args) + self.assertEqual(details.get("discount_percentage"), 0) + + customer.territory = territory + customer.save() + def test_pricing_rule_for_variants(self): from erpnext.stock.get_item_details import get_item_details diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index bbf73e80809..de63ec3a8c4 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): ) frappe.flags.tree_conditions[key] = condition + + elif allow_blank: + condition = f"ifnull({table}.{field}, '') = ''" + return condition diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index fddd9f83926..baf512210dc 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(unittest.TestCase): si.save() si.submit() + original_gle = [ + ["Debtors - _TC", 3000.0, 0, "2023-07-01"], + [deferred_account, 0.0, 3000, "2023-07-01"], + ] + + check_gl_entries(self, si.name, original_gle, "2023-07-01") + process_deferred_accounting = frappe.get_doc( dict( doctype="Process Deferred Accounting", @@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, "2023-07-01") + + # cancel the process deferred accounting document + process_deferred_accounting.cancel() + + # check if gl entries are cancelled + check_gl_entries(self, si.name, original_gle, "2023-07-01") change_acc_settings() def test_pda_submission_and_cancellation(self): diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 40534059711..57d0c59329c 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", { }; }); frm.set_query("account", function () { + if (!frm.doc.company) { + frappe.throw(__("Please set Company")); + } return { filters: { company: frm.doc.company, @@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", { }; }); frm.set_query("cost_center", function () { + if (!frm.doc.company) { + frappe.throw(__("Please set Company")); + } return { filters: { company: frm.doc.company, @@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", { }; }); frm.set_query("project", function () { + if (!frm.doc.company) { + frappe.throw(__("Please set Company")); + } return { filters: { company: frm.doc.company, @@ -79,6 +88,11 @@ frappe.ui.form.on("Process Statement Of Accounts", { frm.set_value("to_date", frappe.datetime.get_today()); } }, + company: function (frm) { + frm.set_value("account", ""); + frm.set_value("cost_center", ""); + frm.set_value("project", ""); + }, report: function (frm) { let filters = { company: frm.doc.company, diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index 4041a8ecad7..f5779cff84f 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -376,7 +376,7 @@ "default": "0", "fieldname": "ignore_exchange_rate_revaluation_journals", "fieldtype": "Check", - "label": "Ignore Exchange Rate Revaluation Journals" + "label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals" }, { "default": "0", @@ -400,7 +400,7 @@ } ], "links": [], - "modified": "2025-04-30 14:43:23.643006", + "modified": "2025-07-08 16:52:12.602384", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 714ed623796..bbf59aa7f02 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 @@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document): # end: auto-generated types def validate(self): + self.validate_account() + self.validate_company_for_table("Cost Center") + self.validate_company_for_table("Project") + if not self.subject: self.subject = "Statement Of Accounts for {{ customer.customer_name }}" if not self.body: @@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document): self.to_date = self.start_date self.from_date = add_months(self.to_date, -1 * self.filter_duration) + def validate_account(self): + if not self.account: + return + + if self.company != frappe.get_cached_value("Account", self.account, "company"): + frappe.throw( + _("Account {0} doesn't belong to Company {1}").format( + frappe.bold(self.account), + frappe.bold(self.company), + ) + ) + + def validate_company_for_table(self, doctype): + field = frappe.scrub(doctype) + if not self.get(field): + return + + fieldname = field + "_name" + + values = set(d.get(fieldname) for d in self.get(field)) + invalid_values = frappe.db.get_all( + doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name" + ) + + if invalid_values: + msg = _("

Following {0}s doesn't belong to Company {1} :

").format( + doctype, frappe.bold(self.company) + ) + + msg += ( + "" + ) + + frappe.throw(_(msg)) + def get_report_pdf(doc, consolidated=True): statement_dict = get_statement_dict(doc) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 35669308091..bae372250ea 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2497,6 +2497,10 @@ class TestSalesInvoice(FrappeTestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) + @change_settings( + "Accounts Settings", + {"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0}, + ) def test_deferred_revenue(self): deferred_account = create_account( account_name="Deferred Revenue", @@ -2551,6 +2555,10 @@ class TestSalesInvoice(FrappeTestCase): self.assertRaises(frappe.ValidationError, si.save) + @change_settings( + "Accounts Settings", + {"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0}, + ) def test_fixed_deferred_revenue(self): deferred_account = create_account( account_name="Deferred Revenue", @@ -2558,10 +2566,6 @@ class TestSalesInvoice(FrappeTestCase): company="_Test Company", ) - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Months" - acc_settings.save() - item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_revenue = 1 item.deferred_revenue_account = deferred_account @@ -2601,10 +2605,6 @@ class TestSalesInvoice(FrappeTestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Days" - acc_settings.save() - def test_validate_inter_company_transaction_address_links(self): def _validate_address_link(address, link_doctype, link_name): return frappe.db.get_value( @@ -2853,7 +2853,9 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(si.items[0].rate, rate) self.assertEqual(target_doc.items[0].rate, rate) - check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) + check_gl_entries( + self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice" + ) def test_internal_transfer_gl_precision_issues(self): # Make a stock queue of an item with two valuations @@ -4587,6 +4589,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type=" ) gl_entries = q.run(as_dict=True) + doc.assertGreater(len(gl_entries), 0) + for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index 7612294a85c..e57b90f11f7 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -12,6 +12,7 @@ from frappe.utils.data import comma_and from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, + get_advance_payment_doctypes, unlink_ref_doc_from_payment_entries, update_voucher_outstanding, ) @@ -84,7 +85,7 @@ class UnreconcilePayment(Document): update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) - if doc.doctype in frappe.get_hooks("advance_payment_doctypes"): + if doc.doctype in get_advance_payment_doctypes(): doc.set_total_advance_paid() frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index c5ee7400473..20476b4164c 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -692,7 +692,18 @@ def make_reverse_gl_entries( query.run() else: if not immutable_ledger_enabled: - set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) + gle_names = [x.get("name") for x in gl_entries] + + # if names are available, cancel only that set of entries + if not all(gle_names): + set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) + else: + frappe.db.sql( + """UPDATE `tabGL Entry` SET is_cancelled = 1, + modified=%s, modified_by=%s + where name in %s and is_cancelled = 0""", + (now(), frappe.session.user, tuple(gle_names)), + ) for entry in gl_entries: new_gle = copy.deepcopy(entry) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e87fa699908..a2237ea9ac2 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -15,7 +15,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, get_dimension_with_children, ) -from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type +from erpnext.accounts.utils import ( + get_advance_payment_doctypes, + get_currency_precision, + get_party_types_from_account_type, +) # This report gives a summary of all Outstanding Invoices considering the following @@ -62,6 +66,7 @@ class ReceivablePayableReport: frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method") or "Buffered Cursor" ) # Fail Safe + self.advance_payment_doctypes = get_advance_payment_doctypes() def run(self, args): self.filters.update(args) @@ -85,6 +90,7 @@ class ReceivablePayableReport: self.party_details = {} self.invoices = set() self.skip_total_row = 0 + self.advance_payment_doctypes = get_advance_payment_doctypes() if self.filters.get("group_by_party"): self.previous_party = "" @@ -181,7 +187,10 @@ class ReceivablePayableReport: if key not in self.voucher_balance: self.voucher_balance[key] = self.build_voucher_dict(ple) - if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no: + if (ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no) or ( + ple.voucher_type in ("Payment Entry", "Journal Entry") + and ple.against_voucher_type in self.advance_payment_doctypes + ): self.voucher_balance[key].cost_center = ple.cost_center self.get_invoices(ple) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index 1c85061a551..9a3c05e90f2 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -79,6 +79,14 @@ class Deferred_Item: return - estimated amount to post for given period Calculated based on already booked amount and item service period """ + if self.filters.book_deferred_entries_based_on == "Months": + # if the deferred entries are based on service period, use service start and end date + return self.calculate_monthly_amount(start_date, end_date) + + else: + return self.calculate_days_amount(start_date, end_date) + + def calculate_monthly_amount(self, start_date, end_date): total_months = ( (self.service_end_date.year - self.service_start_date.year) * 12 + (self.service_end_date.month - self.service_start_date.month) @@ -105,6 +113,19 @@ class Deferred_Item: return base_amount + def calculate_days_amount(self, start_date, end_date): + base_amount = 0 + total_days = date_diff(self.service_end_date, self.service_start_date) + 1 + total_booking_days = date_diff(end_date, start_date) + 1 + already_booked_amount = self.get_item_total() + + base_amount = flt(self.base_net_amount * total_booking_days / flt(total_days)) + + if base_amount + already_booked_amount > self.base_net_amount: + base_amount = self.base_net_amount - already_booked_amount + + return base_amount + def make_dummy_gle(self, name, date, amount): """ return - frappe._dict() of a dummy gle entry @@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report: else: self.filters = frappe._dict(filters) + self.filters.book_deferred_entries_based_on = frappe.db.get_singles_value( + "Accounts Settings", "book_deferred_entries_based_on" + ) + self.period_list = None self.deferred_invoices = [] # holds period wise total for report @@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report: .join(inv) .on(inv.name == inv_item.parent) .left_join(gle) - .on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account)) + .on( + (inv_item.name == gle.voucher_detail_no) + & (deferred_account_field == gle.account) + & (gle.is_cancelled == 0) + ) .select( inv.name.as_("doc"), inv.posting_date, diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 0d1e185382d..71e541692f8 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -9,7 +9,7 @@ import re import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Max, Min, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate from pypika.terms import ExistsCriterion @@ -109,14 +109,19 @@ def get_period_list( def get_fiscal_year_data(from_fiscal_year, to_fiscal_year): - fiscal_year = frappe.db.sql( - """select min(year_start_date) as year_start_date, - max(year_end_date) as year_end_date from `tabFiscal Year` where - name between %(from_fiscal_year)s and %(to_fiscal_year)s""", - {"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year}, - as_dict=1, + from_year_start_date = frappe.get_cached_value("Fiscal Year", from_fiscal_year, "year_start_date") + to_year_end_date = frappe.get_cached_value("Fiscal Year", to_fiscal_year, "year_end_date") + + fy = frappe.qb.DocType("Fiscal Year") + + query = ( + frappe.qb.from_(fy) + .select(Min(fy.year_start_date).as_("year_start_date"), Max(fy.year_end_date).as_("year_end_date")) + .where(fy.year_start_date >= from_year_start_date) + .where(fy.year_end_date <= to_year_end_date) ) + fiscal_year = query.run(as_dict=True) return fiscal_year[0] if fiscal_year else {} diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 4039ea27ebd..f27441bc3e2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -209,7 +209,7 @@ frappe.query_reports["General Ledger"] = { }, { fieldname: "ignore_err", - label: __("Ignore Exchange Rate Revaluation Journals"), + label: __("Ignore Exchange Rate Revaluation and Gain / Loss Journals"), fieldtype: "Check", }, { diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index e1ce1bd35a2..d2a7e8256b3 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -6,12 +6,9 @@ from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import flt, today -from erpnext.accounts.doctype.account.test_account import create_account -from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.general_ledger.general_ledger import execute from erpnext.controllers.sales_and_purchase_return import make_return_doc -from erpnext.selling.doctype.customer.test_customer import create_internal_customer class TestGeneralLedger(FrappeTestCase): @@ -171,90 +168,6 @@ class TestGeneralLedger(FrappeTestCase): self.assertEqual(data[3]["debit"], 100) self.assertEqual(data[3]["credit"], 100) - @change_settings("Accounts Settings", {"delete_linked_ledger_entries": True}) - def test_debit_in_exchange_gain_loss_account(self): - company = "_Test Company" - - exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account") - if not exchange_gain_loss_account: - frappe.db.set_value( - "Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" - ) - - account_name = "_Test Receivable USD - _TC" - customer_name = "_Test Customer USD" - - sales_invoice = create_sales_invoice( - company=company, - customer=customer_name, - currency="USD", - debit_to=account_name, - conversion_rate=85, - posting_date=today(), - ) - - payment_entry = create_payment_entry( - company=company, - party_type="Customer", - party=customer_name, - payment_type="Receive", - paid_from=account_name, - paid_from_account_currency="USD", - paid_to="Cash - _TC", - paid_to_account_currency="INR", - paid_amount=10, - do_not_submit=True, - ) - payment_entry.base_paid_amount = 800 - payment_entry.received_amount = 800 - payment_entry.currency = "USD" - payment_entry.source_exchange_rate = 80 - payment_entry.append( - "references", - frappe._dict( - { - "reference_doctype": "Sales Invoice", - "reference_name": sales_invoice.name, - "total_amount": 10, - "outstanding_amount": 10, - "exchange_rate": 85, - "allocated_amount": 10, - "exchange_gain_loss": -50, - } - ), - ) - payment_entry.save() - payment_entry.submit() - - journal_entry = frappe.get_all( - "Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"] - ) - - columns, data = execute( - frappe._dict( - { - "company": company, - "from_date": today(), - "to_date": today(), - "include_dimensions": 1, - "include_default_book_entries": 1, - "account": ["_Test Exchange Gain/Loss - _TC"], - "categorize_by": "Categorize by Voucher (Consolidated)", - } - ) - ) - - entry = data[1] - self.assertEqual(entry["debit"], 50) - self.assertEqual(entry["voucher_type"], "Journal Entry") - self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"]) - - payment_entry.cancel() - payment_entry.delete() - sales_invoice.reload() - sales_invoice.cancel() - sales_invoice.delete() - def test_ignore_exchange_rate_journals_filter(self): # create a new account with USD currency account_name = "Test Debtors USD" 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 db93c0ef325..73d8c3f7d14 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 @@ -5,13 +5,14 @@ import frappe from frappe import _ from frappe.model.meta import get_field_precision +from frappe.query_builder import functions as fn from frappe.utils import cstr, flt from frappe.utils.nestedset import get_descendants_of from frappe.utils.xlsxutils import handle_html from pypika import Order from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments -from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns +from erpnext.accounts.report.utils import get_values_for_columns from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import ( get_customer_details, ) @@ -433,7 +434,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None): si.is_internal_customer, si.customer, si.remarks, - si.territory, + fn.IfNull(si.territory, "Not Specified").as_("territory"), si.company, si.base_net_total, sii.project, @@ -456,7 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None): sii.base_net_rate, sii.base_net_amount, si.customer_name, - si.customer_group, + fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"), sii.so_detail, si.update_stock, sii.uom, diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index ee52da291b1..4507fbdcb60 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -121,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ ) out.append(row) - out.sort(key=lambda x: x["section_code"]) + out.sort(key=lambda x: (x["section_code"], x["transaction_date"])) return out diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py index 6eff81e7f42..853ae71abe3 100644 --- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py @@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): mid_year = add_to_date(fiscal_year[1], months=6) tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3") tds_doc.rates[0].to_date = mid_year + from_date = add_to_date(mid_year, days=1) tds_doc.append( "rates", { "tax_withholding_rate": 20, - "from_date": add_to_date(mid_year, days=1), + "from_date": from_date, "to_date": fiscal_year[2], "single_threshold": 1, "cumulative_threshold": 1, @@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase): tds_doc.save() - inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_1 = make_purchase_invoice( + rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True + ) + inv_1.set_posting_time = 1 inv_1.apply_tds = 1 - inv_1.tax_withholding_category = "TDS - 3" + inv_1.tax_withholding_category = tds_doc.name + inv_1.save() inv_1.submit() - inv_2 = make_purchase_invoice( - rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True - ) + inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True) inv_2.set_posting_time = 1 - - inv_1.apply_tds = 1 - inv_2.tax_withholding_category = "TDS - 3" + inv_2.apply_tds = 1 + inv_2.tax_withholding_category = tds_doc.name inv_2.save() inv_2.submit() diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 02ba54604c4..2a72b10e4eb 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -107,11 +107,7 @@ def convert_to_presentation_currency(gl_entries, currency_info): credit_in_account_currency = flt(entry["credit_in_account_currency"]) account_currency = entry["account_currency"] - if ( - len(account_currencies) == 1 - and account_currency == presentation_currency - and (debit_in_account_currency or credit_in_account_currency) - ): + if len(account_currencies) == 1 and account_currency == presentation_currency: entry["debit"] = debit_in_account_currency entry["credit"] = credit_in_account_currency else: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 6fa0e3e9802..c775773cd71 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -169,7 +169,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None): if doc: doc.fiscal_year = years[0] else: - throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year)) + throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year)) @frappe.whitelist() @@ -629,6 +629,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): # Update Advance Paid in SO/PO since they might be getting unlinked update_advance_paid = [] + if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]: update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name)) @@ -713,23 +714,8 @@ def update_reference_in_payment_entry( update_advance_paid = [] # Update Reconciliation effect date in reference - reconciliation_takes_effect_on = frappe.get_cached_value( - "Company", payment_entry.company, "reconciliation_takes_effect_on" - ) if payment_entry.book_advance_payments_in_separate_party_account: - if reconciliation_takes_effect_on == "Advance Payment Date": - reconcile_on = payment_entry.posting_date - elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": - date_field = "posting_date" - if d.against_voucher_type in ["Sales Order", "Purchase Order"]: - date_field = "transaction_date" - reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field) - - if getdate(reconcile_on) < getdate(payment_entry.posting_date): - reconcile_on = payment_entry.posting_date - elif reconciliation_takes_effect_on == "Reconciliation Date": - reconcile_on = nowdate() - + reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date) reference_details.update({"reconcile_effect_on": reconcile_on}) if d.voucher_detail_no: @@ -783,6 +769,28 @@ def update_reference_in_payment_entry( return row, update_advance_paid +def get_reconciliation_effect_date(reference, company, posting_date): + reconciliation_takes_effect_on = frappe.get_cached_value( + "Company", company, "reconciliation_takes_effect_on" + ) + + if reconciliation_takes_effect_on == "Advance Payment Date": + reconcile_on = posting_date + elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + date_field = "posting_date" + if reference.against_voucher_type in ["Sales Order", "Purchase Order"]: + date_field = "transaction_date" + reconcile_on = frappe.db.get_value( + reference.against_voucher_type, reference.against_voucher, date_field + ) + if getdate(reconcile_on) < getdate(posting_date): + reconcile_on = posting_date + elif reconciliation_takes_effect_on == "Reconciliation Date": + reconcile_on = nowdate() + + return reconcile_on + + def cancel_exchange_gain_loss_journal( parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None ) -> None: @@ -997,58 +1005,79 @@ def remove_ref_doc_link_from_pe( per = qb.DocType("Payment Entry Reference") pay = qb.DocType("Payment Entry") - linked_pe = ( + query = ( qb.from_(per) - .select(per.parent) - .where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))) - .run(as_list=1) - ) - linked_pe = convert_to_list(linked_pe) - # remove reference only from specified payment - linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe - - if linked_pe: - update_query = ( - qb.update(per) - .set(per.allocated_amount, 0) - .set(per.modified, now()) - .set(per.modified_by, frappe.session.user) - .where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + .select("*") + .where( + (per.reference_doctype == ref_type) + & (per.reference_name == ref_no) + & (per.docstatus.lt(2)) + & (per.parenttype == "Payment Entry") ) + ) - if payment_name: - update_query = update_query.where(per.parent == payment_name) + # update reference only from specified payment + if payment_name: + query = query.where(per.parent == payment_name) - update_query.run() + reference_rows = query.run(as_dict=True) - for pe in linked_pe: - try: - pe_doc = frappe.get_doc("Payment Entry", pe) - pe_doc.set_amounts() + if not reference_rows: + return - # Call cancel on only removed reference - references = [ - x - for x in pe_doc.references - if x.reference_doctype == ref_type and x.reference_name == ref_no - ] - [pe_doc.make_advance_gl_entries(x, cancel=1) for x in references] + linked_pe = set() + row_names = set() - pe_doc.clear_unallocated_reference_document_rows() - pe_doc.validate_payment_type_with_outstanding() - except Exception: - msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) - msg += "
" - msg += _("Please cancel payment entry manually first") - frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) + for row in reference_rows: + linked_pe.add(row.parent) + row_names.add(row.name) - qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set( - pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount - ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set( - pay.modified_by, frappe.session.user - ).where(pay.name == pe).run() + from erpnext.accounts.doctype.payment_request.payment_request import ( + update_payment_requests_as_per_pe_references, + ) - frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) + # Update payment request amount + update_payment_requests_as_per_pe_references(reference_rows, cancel=True) + + # Update allocated amounts and modified fields in one go + ( + qb.update(per) + .set(per.allocated_amount, 0) + .set(per.modified, now()) + .set(per.modified_by, frappe.session.user) + .where(per.name.isin(row_names)) + .where(per.parenttype == "Payment Entry") + .run() + ) + + for pe in linked_pe: + try: + pe_doc = frappe.get_doc("Payment Entry", pe) + pe_doc.set_amounts() + + # Call cancel on only removed reference + references = [x for x in pe_doc.references if x.name in row_names] + [pe_doc.make_advance_gl_entries(x, cancel=1) for x in references] + + pe_doc.clear_unallocated_reference_document_rows() + pe_doc.validate_payment_type_with_outstanding() + except Exception: + msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) + msg += "
" + msg += _("Please cancel payment entry manually first") + frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) + + ( + qb.update(pay) + .set(pay.total_allocated_amount, pe_doc.total_allocated_amount) + .set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount) + .set(pay.unallocated_amount, pe_doc.unallocated_amount) + .set(pay.modified, now()) + .set(pay.modified_by, frappe.session.user) + .where(pay.name == pe) + .run() + ) + frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) @frappe.whitelist() @@ -2235,6 +2264,15 @@ def get_party_types_from_account_type(account_type): return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name") +def get_advance_payment_doctypes(): + """ + Get list of advance payment doctypes based on type. + :param type: Optional, can be "receivable" or "payable". If not provided, returns both. + """ + + return frappe.get_hooks("advance_payment_doctypes") + + def run_ledger_health_checks(): health_monitor_settings = frappe.get_doc("Ledger Health Monitor") if health_monitor_settings.enable_health_monitor: diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 43ef287854e..e29e695f48c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -807,11 +807,19 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.credit_to = get_party_account("Supplier", source.supplier, source.company) def update_item(obj, target, source_parent): - target.amount = flt(obj.amount) - flt(obj.billed_amt) - target.base_amount = target.amount * flt(source_parent.conversion_rate) - target.qty = ( - target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty) - ) + def get_billed_qty(po_item_name): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty).as_("qty")) + .where((table.docstatus == 1) & (table.po_detail == po_item_name)) + ) + return query.run(pluck="qty")[0] or 0 + + billed_qty = flt(get_billed_qty(obj.name)) + target.qty = flt(obj.qty) - billed_qty item = get_item_defaults(target.item_code, source_parent.company) item_group = get_item_group_defaults(target.item_code, source_parent.company) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index c6ee35d6090..206cbc0000e 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1286,6 +1286,25 @@ class TestPurchaseOrder(FrappeTestCase): self.assertFalse(po.per_billed) self.assertEqual(po.status, "To Receive and Bill") + @change_settings("Buying Settings", {"maintain_same_rate": 0}) + def test_purchase_invoice_creation_with_partial_qty(self): + po = create_purchase_order(qty=100, rate=10) + + pi = make_pi_from_po(po.name) + pi.items[0].qty = 42 + pi.items[0].rate = 7.5 + pi.submit() + + pi = make_pi_from_po(po.name) + self.assertEqual(pi.items[0].qty, 58) + self.assertEqual(pi.items[0].rate, 10) + pi.items[0].qty = 8 + pi.items[0].rate = 5 + pi.submit() + + pi = make_pi_from_po(po.name) + self.assertEqual(pi.items[0].qty, 50) + def create_po_for_sc_testing(): from erpnext.controllers.tests.test_subcontracting_controller import ( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cf0707d9efa..33c5783861d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -51,6 +51,9 @@ from erpnext.accounts.utils import ( get_fiscal_years, validate_fiscal_year, ) +from erpnext.accounts.utils import ( + get_advance_payment_doctypes as _get_advance_payment_doctypes, +) from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -386,9 +389,7 @@ class AccountsController(TransactionBase): adv = qb.DocType("Advance Payment Ledger Entry") qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run() - advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes") - - if self.doctype in advance_payment_doctypes: + if self.doctype in self.get_advance_payment_doctypes(): qb.from_(adv).delete().where( adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name) ).run() @@ -2912,7 +2913,7 @@ class AccountsController(TransactionBase): repost_ledger.submit() def get_advance_payment_doctypes(self) -> list: - return frappe.get_hooks("advance_payment_doctypes") + return _get_advance_payment_doctypes() def make_advance_payment_ledger_for_journal(self): advance_payment_doctypes = self.get_advance_payment_doctypes() @@ -3965,6 +3966,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ).format(frappe.bold(parent.name)) ) else: # Sales Order + parent.validate_selling_price() parent.validate_for_duplicate_items() parent.validate_warehouse() parent.update_reserved_qty() diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 667e07ab9ff..81cf7fb80d7 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1648,8 +1648,9 @@ def make_quality_inspections(doctype, docname, items): "sample_size": flt(item.get("sample_size")), "item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None, "batch_no": item.get("batch_no"), + "child_row_reference": item.get("child_row_reference"), } - ).insert() + ) quality_inspection.save() inspections.append(quality_inspection.name) @@ -1662,14 +1663,9 @@ def is_reposting_pending(): ) -def future_sle_exists(args, sl_entries=None, allow_force_reposting=True): +def future_sle_exists(args, sl_entries=None): from erpnext.stock.utils import get_combine_datetime - if allow_force_reposting and frappe.db.get_single_value( - "Stock Reposting Settings", "do_reposting_for_each_stock_transaction" - ): - return True - key = (args.voucher_type, args.voucher_no) if not hasattr(frappe.local, "future_sle"): frappe.local.future_sle = {} diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index ba6f604960c..22a71734f3d 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -601,12 +601,15 @@ class SubcontractingController(StockController): rm_obj.use_serial_batch_fields = 1 self.__set_batch_nos(bom_item, item_row, rm_obj, qty) - if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields: - rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle( - item_row, rm_obj, rm_obj.consumed_qty - ) + if self.doctype == "Subcontracting Receipt": + if not use_serial_batch_fields: + rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle( + item_row, rm_obj, rm_obj.consumed_qty + ) - self.set_rate_for_supplied_items(rm_obj, item_row) + self.set_rate_for_supplied_items(rm_obj, item_row) + elif self.backflush_based_on == "BOM": + self.update_rate_for_supplied_items() def update_rate_for_supplied_items(self): if self.doctype != "Subcontracting Receipt": diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 5a6edb703fa..bf6bf2530ce 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -987,7 +987,7 @@ class BOM(WebsiteGenerator): self.transfer_material_against = "Work Order" if not self.transfer_material_against and not self.is_new(): frappe.throw( - _("Setting {} is required").format(self.meta.get_label("transfer_material_against")), + _("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))), title=_("Missing value"), ) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 690d7945beb..f95946a1724 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -51,9 +51,13 @@ frappe.ui.form.on("Job Card", { let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; if (to_request || excess_transfer_allowed) { - frm.add_custom_button(__("Material Request"), () => { - frm.trigger("make_material_request"); - }); + frm.add_custom_button( + __("Material Request"), + () => { + frm.trigger("make_material_request"); + }, + __("Create") + ); } // check if any row has untransferred materials @@ -61,9 +65,13 @@ frappe.ui.form.on("Job Card", { let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty); if (to_transfer || excess_transfer_allowed) { - frm.add_custom_button(__("Material Transfer"), () => { - frm.trigger("make_stock_entry"); - }).addClass("btn-primary"); + frm.add_custom_button( + __("Material Transfer"), + () => { + frm.trigger("make_stock_entry"); + }, + __("Create") + ); } } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 176e955ee72..9b1bf28f997 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -390,7 +390,7 @@ class WorkOrder(Document): if qty > completed_qty: frappe.throw( _("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format( - self.meta.get_label(fieldname), qty, completed_qty, self.name + _(self.meta.get_label(fieldname)), qty, completed_qty, self.name ), StockOverProductionError, ) @@ -1077,7 +1077,7 @@ class WorkOrder(Document): self.transfer_material_against = "Work Order" if not self.transfer_material_against: frappe.throw( - _("Setting {} is required").format(self.meta.get_label("transfer_material_against")), + _("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))), title=_("Missing value"), ) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 34140dc2c84..71cb012c1b8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -411,3 +411,4 @@ erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies +erpnext.patches.v15_0.set_company_on_pos_inv_merge_log diff --git a/erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py b/erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py new file mode 100644 index 00000000000..8f83898a877 --- /dev/null +++ b/erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py @@ -0,0 +1,12 @@ +import frappe + + +def execute(): + pos_invoice_merge_logs = frappe.db.get_all( + "POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"] + ) + + for log in pos_invoice_merge_logs: + if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry): + company = frappe.db.get_value("POS Closing Entry", log.pos_closing_entry, "company") + frappe.db.set_value("POS Invoice Merge Log", log.name, "company", company) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 45baff562ce..449a9d87ff2 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -202,6 +202,12 @@ frappe.ui.form.on("Project", { }); }); }, + + collect_progress: function (frm) { + if (frm.doc.collect_progress && !frm.doc.subject) { + frm.set_value("subject", __("For project {0}, update your status", [frm.doc.name])); + } + }, }); function open_form(frm, doctype, child_doctype, parentfield) { diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 464b1c9d7a8..e2613465dd3 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -62,6 +62,7 @@ "day_to_send", "weekly_time_to_send", "column_break_45", + "subject", "message" ], "fields": [ @@ -447,6 +448,13 @@ "print_hide": 1, "reqd": 1, "set_only_once": 1 + }, + { + "depends_on": "collect_progress", + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject", + "mandatory_depends_on": "collect_progress" } ], "icon": "fa fa-puzzle-piece", @@ -454,7 +462,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2024-04-24 10:56:16.001032", + "modified": "2025-07-03 10:54:30.444139", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -501,6 +509,7 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "search_fields": "project_name,customer, status, priority, is_active", "show_name_in_global_search": 1, "sort_field": "modified", @@ -509,4 +518,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index f7653d92d63..ebfff79531f 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -62,6 +62,7 @@ class Project(Document): sales_order: DF.Link | None second_email: DF.Time | None status: DF.Literal["Open", "Completed", "Cancelled"] + subject: DF.Data | None to_time: DF.Time | None total_billable_amount: DF.Currency total_billed_amount: DF.Currency @@ -606,8 +607,6 @@ def send_project_update_email_to_users(project): } ).insert() - subject = "For project %s, update your status" % (project) - incoming_email_account = frappe.db.get_value( "Email Account", dict(enable_incoming=1, default_incoming=1), "email_id" ) @@ -615,7 +614,7 @@ def send_project_update_email_to_users(project): frappe.sendmail( recipients=get_users_email(doc), message=doc.message, - subject=_(subject), + subject=doc.subject, reference_doctype=project_update.doctype, reference_name=project_update.name, reply_to=incoming_email_account, diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 2b854d649d8..2162c000221 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -581,7 +581,8 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { transaction_date: frm.doc.transaction_date || frm.doc.posting_date, ignore_pricing_rule: frm.doc.ignore_pricing_rule, doctype: frm.doc.doctype - } + }, + price_list: frm.doc.price_list, }, freeze: true, callback: function(r) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3f6f9ca5e20..76ff4e8c49d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -371,6 +371,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "inspection_type": inspection_type, "reference_type": me.frm.doc.doctype, "reference_name": me.frm.doc.name, + "child_row_reference": row.doc.name, "item_code": row.doc.item_code, "description": row.doc.description, "item_serial_no": row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null, @@ -385,7 +386,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe docstatus: ["<", 2], inspection_type: inspection_type, reference_name: doc.name, - item_code: d.item_code + item_code: d.item_code, + child_row_reference : d.name } } }); @@ -2427,12 +2429,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe fields: fields, primary_action: function () { const data = dialog.get_values(); + const selected_data = data.items.filter(item => item?.__checked == 1 ); frappe.call({ method: "erpnext.controllers.stock_controller.make_quality_inspections", args: { doctype: me.frm.doc.doctype, docname: me.frm.doc.name, - items: data.items + items: selected_data, }, freeze: true, callback: function (r) { diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index f51e83ffe31..c9e2361e842 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -1063,21 +1063,31 @@ justify-content: flex-end; padding-right: var(--padding-sm); - > .customer-name { - font-size: var(--text-2xl); - font-weight: 700; - } + > .customer-section { + margin-bottom: auto; - > .customer-email { - font-size: var(--text-md); - font-weight: 500; + > .customer-name { + font-size: var(--text-2xl); + font-weight: 700; + } + + > .customer-code { + font-size: var(--text-xs); + font-weight: 500; + color: var(--text-light); + } + + > .customer-email { + font-size: var(--text-md); + font-weight: 500; + } } > .cashier { font-size: var(--text-md); font-weight: 500; color: var(--gray-600); - margin-top: auto; + margin-top: var(--margin-md); } } @@ -1085,7 +1095,6 @@ display: flex; flex-direction: column; align-items: flex-end; - justify-content: space-between; > .paid-amount { font-size: var(--text-2xl); diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 685b27ab3ef..da8476d6b1f 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -117,14 +117,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { if ( - frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation || - !doc.valid_till || - frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0 + frappe.model.can_create("Sales Order") && + (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation || + !doc.valid_till || + frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) ) { this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create")); } - if (doc.status !== "Ordered") { + if (doc.status !== "Ordered" && this.frm.has_perm("write")) { this.frm.add_custom_button(__("Set as Lost"), () => { this.frm.trigger("set_as_lost_dialog"); }); @@ -133,7 +134,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. cur_frm.page.set_inner_btn_group_as_primary(__("Create")); } - if (this.frm.doc.docstatus === 0) { + if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) { this.frm.add_custom_button( __("Opportunity"), function () { diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 89e001897dd..d8de762dcd3 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -338,7 +338,7 @@ def create_opening_voucher(pos_profile, company, balance_details): @frappe.whitelist() def get_past_order_list(search_term, status, limit=20): - fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"] + fields = ["name", "grand_total", "currency", "customer", "customer_name", "posting_time", "posting_date"] invoice_list = [] if search_term and status: diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js index dda44f25299..5ea58a43c09 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js @@ -106,7 +106,7 @@ erpnext.PointOfSale.PastOrderList = class { - ${frappe.ellipsis(invoice.customer, 20)} + ${frappe.ellipsis(invoice.customer_name, 20)}
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index cf775176c07..0a965c47f48 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -73,14 +73,18 @@ erpnext.PointOfSale.PastOrderSummary = class { get_upper_section_html(doc) { const { status } = doc; let indicator_color = ""; + const is_customer_naming_by_customer_name = frappe.sys_defaults.cust_master_name !== "Customer Name"; ["Paid", "Consolidated"].includes(status) && (indicator_color = "green"); status === "Draft" && (indicator_color = "red"); status === "Return" && (indicator_color = "grey"); return `
-
${doc.customer}
-
${this.customer_email}
+
+
${doc.customer_name}
+ ${is_customer_naming_by_customer_name ? `
${doc.customer}
` : ""} +
${this.customer_email}
+
${__("Sold by")}: ${doc.owner}
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 610ef6205ee..c00b947b295 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -395,7 +395,7 @@ class EmailDigest(Document): label = get_link_to_report( "General Ledger", - self.meta.get_label("income"), + _(self.meta.get_label("income")), filters={ "from_date": self.future_from_date, "to_date": self.future_to_date, @@ -427,7 +427,7 @@ class EmailDigest(Document): filters = {"currency": self.currency} label = get_link_to_report( "Profit and Loss Statement", - label=self.meta.get_label(root_type + "_year_to_date"), + label=_(self.meta.get_label(root_type + "_year_to_date")), filters=filters, ) @@ -435,7 +435,7 @@ class EmailDigest(Document): filters = {"currency": self.currency} label = get_link_to_report( "Profit and Loss Statement", - label=self.meta.get_label(root_type + "_year_to_date"), + label=_(self.meta.get_label(root_type + "_year_to_date")), filters=filters, ) @@ -466,7 +466,7 @@ class EmailDigest(Document): label = get_link_to_report( "General Ledger", - self.meta.get_label("expenses_booked"), + _(self.meta.get_label("expenses_booked")), filters={ "company": self.company, "from_date": self.future_from_date, @@ -500,7 +500,7 @@ class EmailDigest(Document): label = get_link_to_report( "Sales Order", - label=self.meta.get_label("sales_orders_to_bill"), + label=_(self.meta.get_label("sales_orders_to_bill")), report_type="Report Builder", doctype="Sales Order", filters={ @@ -526,7 +526,7 @@ class EmailDigest(Document): label = get_link_to_report( "Sales Order", - label=self.meta.get_label("sales_orders_to_deliver"), + label=_(self.meta.get_label("sales_orders_to_deliver")), report_type="Report Builder", doctype="Sales Order", filters={ @@ -552,7 +552,7 @@ class EmailDigest(Document): label = get_link_to_report( "Purchase Order", - label=self.meta.get_label("purchase_orders_to_receive"), + label=_(self.meta.get_label("purchase_orders_to_receive")), report_type="Report Builder", doctype="Purchase Order", filters={ @@ -578,7 +578,7 @@ class EmailDigest(Document): label = get_link_to_report( "Purchase Order", - label=self.meta.get_label("purchase_orders_to_bill"), + label=_(self.meta.get_label("purchase_orders_to_bill")), report_type="Report Builder", doctype="Purchase Order", filters={ @@ -630,7 +630,7 @@ class EmailDigest(Document): "company": self.company, } label = get_link_to_report( - "Account Balance", label=self.meta.get_label(fieldname), filters=filters + "Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters ) else: filters = { @@ -640,7 +640,7 @@ class EmailDigest(Document): "company": self.company, } label = get_link_to_report( - "Account Balance", label=self.meta.get_label(fieldname), filters=filters + "Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters ) return {"label": label, "value": balance, "last_value": prev_balance} @@ -648,17 +648,17 @@ class EmailDigest(Document): if account_type == "Payable": label = get_link_to_report( "Accounts Payable", - label=self.meta.get_label(fieldname), + label=_(self.meta.get_label(fieldname)), filters={"report_date": self.future_to_date, "company": self.company}, ) elif account_type == "Receivable": label = get_link_to_report( "Accounts Receivable", - label=self.meta.get_label(fieldname), + label=_(self.meta.get_label(fieldname)), filters={"report_date": self.future_to_date, "company": self.company}, ) else: - label = self.meta.get_label(fieldname) + label = _(self.meta.get_label(fieldname)) return {"label": label, "value": balance, "last_value": prev_balance, "count": count} @@ -748,7 +748,7 @@ class EmailDigest(Document): label = get_link_to_report( "Quotation", - label=self.meta.get_label(fieldname), + label=_(self.meta.get_label(fieldname)), report_type="Report Builder", doctype="Quotation", filters={ @@ -779,7 +779,7 @@ class EmailDigest(Document): label = get_link_to_report( doc_type, - label=self.meta.get_label(fieldname), + label=_(self.meta.get_label(fieldname)), report_type="Report Builder", filters=filters, doctype=doc_type, diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 338fd863ffc..79e9776b91c 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -264,7 +264,7 @@ def update_qty(bin_name, args): actual_qty = bin_details.actual_qty or 0.0 # actual qty is not up to date in case of backdated transaction - if future_sle_exists(args, allow_force_reposting=False): + if future_sle_exists(args): actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse")) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 12182ae990c..37e2737391a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -1281,6 +1281,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "doctype": target_doctype, "postprocess": update_details, "field_no_map": ["taxes_and_charges", "set_warehouse"], + "field_map": {"shipping_address_name": "shipping_address"}, }, doctype + " Item": { "doctype": target_doctype + " Item", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index c819d17b1e7..1b5f4a5743f 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", { set_parent_fields(frm) { if (frm.doc.apply_to_all_doctypes) { - frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document); + let options = ["\n", frm.doc.reference_document]; + + frm.set_df_property("fetch_from_parent", "options", options); } else if (frm.doc.document_type && frm.doc.istable) { frappe.call({ method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields", @@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", { }, callback: (r) => { if (r.message && r.message.length) { - frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message)); + frm.set_df_property("fetch_from_parent", "options", ["\n"].concat(r.message)); } else { frm.set_df_property("fetch_from_parent", "hidden", 1); } diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 9638c4e6289..20250622fda 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -143,7 +143,6 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:!doc.apply_to_all_doctypes", "description": "Set fieldname from which you want to fetch the data from the parent form.", "fieldname": "fetch_from_parent", "fieldtype": "Select", @@ -189,7 +188,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-07-08 08:58:50.228211", + "modified": "2025-07-07 15:51:29.329064", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index b08a602fdf2..6f8571ea156 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -64,16 +64,11 @@ class InventoryDimension(Document): self.reset_value() self.set_source_and_target_fieldname() self.set_type_of_transaction() - self.set_fetch_value_from() def set_type_of_transaction(self): if self.apply_to_all_doctypes: self.type_of_transaction = "Both" - def set_fetch_value_from(self): - if self.apply_to_all_doctypes: - self.fetch_from_parent = self.reference_document - def do_not_update_document(self): if self.is_new() or not self.has_stock_ledger(): return diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index f8128ce0033..40b87b79f97 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -155,6 +155,8 @@ class TestInventoryDimension(FrappeTestCase): reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1 ) + inv_dimension.db_set("fetch_from_parent", "Rack") + self.assertEqual(inv_dimension.type_of_transaction, "Both") self.assertEqual(inv_dimension.fetch_from_parent, "Rack") diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 0536caf61a1..3409e9a559a 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -634,7 +634,7 @@ class Item(Document): if new_properties != [cstr(self.get(field)) for field in field_list]: msg = _("To merge, following properties must be same for both items") - msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) + msg += ": \n" + ", ".join([_(self.meta.get_label(fld)) for fld in field_list]) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): @@ -981,7 +981,7 @@ class Item(Document): return if linked_doc := self._get_linked_submitted_documents(changed_fields): - changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields] + changed_field_labels = [frappe.bold(_(self.meta.get_label(f))) for f in changed_fields] msg = _( "As there are existing submitted transactions against item {0}, you can not change the value of {1}." ).format(self.name, ", ".join(changed_field_labels)) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index c80bcc8123b..a70af1fc384 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -332,5 +332,6 @@ def get_pr_items(purchase_receipt): (pr_item.parent == purchase_receipt.receipt_document) & ((item.is_stock_item == 1) | (item.is_fixed_asset == 1)) ) + .orderby(pr_item.idx) .run(as_dict=True) ) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 18d8919a668..8f5d08fe946 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -42,6 +42,15 @@ frappe.ui.form.on("Material Request", { }, }; }); + + frm.set_query("price_list", () => { + return { + filters: { + buying: 1, + enabled: 1, + }, + }; + }); }, onload: function (frm) { @@ -70,6 +79,7 @@ frappe.ui.form.on("Material Request", { }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + frm.doc.price_list = frappe.defaults.get_default("buying_price_list"); }, company: function (frm) { @@ -245,7 +255,9 @@ frappe.ui.form.on("Material Request", { from_warehouse: item.from_warehouse, warehouse: item.warehouse, doctype: frm.doc.doctype, - buying_price_list: frappe.defaults.get_default("buying_price_list"), + buying_price_list: frm.doc.price_list + ? frm.doc.price_list + : frappe.defaults.get_default("buying_price_list"), currency: frappe.defaults.get_default("Currency"), name: frm.doc.name, qty: item.qty || 1, diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index 8df61fe7fb8..4079288009a 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -16,6 +16,7 @@ "column_break_2", "transaction_date", "schedule_date", + "price_list", "amended_from", "warehouse_section", "scan_barcode", @@ -351,13 +352,19 @@ { "fieldname": "column_break_13", "fieldtype": "Column Break" + }, + { + "fieldname": "price_list", + "fieldtype": "Link", + "label": "Price List", + "options": "Price List" } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2025-04-21 18:36:04.827917", + "modified": "2025-07-07 13:15:28.615984", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", @@ -425,10 +432,11 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "search_fields": "status,transaction_date", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 956f47bb978..8c80f907093 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -8,6 +8,7 @@ import json import frappe +import frappe.defaults from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum @@ -45,6 +46,7 @@ class MaterialRequest(BuyingController): naming_series: DF.Literal["MAT-MR-.YYYY.-"] per_ordered: DF.Percent per_received: DF.Percent + price_list: DF.Link | None scan_barcode: DF.Data | None schedule_date: DF.Date | None select_print_heading: DF.Link | None @@ -151,6 +153,9 @@ class MaterialRequest(BuyingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + if not self.price_list: + self.price_list = frappe.defaults.get_defaults().buying_price_list + def before_update_after_submit(self): self.validate_schedule_date() @@ -764,10 +769,11 @@ def raise_work_orders(material_request): "material_request_item": d.name, "planned_start_date": mr.transaction_date, "company": mr.company, + "project": d.project, } ) - wo_order.set_work_order_operations() + wo_order.get_items_and_operations_from_bom() wo_order.flags.ignore_validate = True wo_order.flags.ignore_mandatory = True wo_order.save() diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index eaeb04d568e..3d3bfc7e010 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -7,6 +7,7 @@ import json import frappe +import frappe.defaults from frappe.model.document import Document from frappe.utils import flt @@ -339,12 +340,20 @@ def on_doctype_update(): @frappe.whitelist() -def get_items_from_product_bundle(row): +def get_items_from_product_bundle(row, price_list): row, items = json.loads(row), [] bundled_items = get_product_bundle_items(row["item_code"]) for item in bundled_items: - row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)}) + row.update( + { + "item_code": item.item_code, + "qty": flt(row["quantity"]) * flt(item.qty), + "conversion_rate": 1, + "price_list": price_list, + "currency": frappe.defaults.get_defaults().currency, + } + ) items.append(get_item_details(row)) return items diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 021b7b1cf17..58aa18359df 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -97,51 +97,25 @@ class QualityInspection(Document): 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", - ) + child_doc = frappe.qb.DocType(doctype) + qi_doc = frappe.qb.DocType("Quality Inspection") - if not child_row_references: - return + child_row_references = ( + frappe.qb.from_(child_doc) + .left_join(qi_doc) + .on(child_doc.name == qi_doc.child_row_reference) + .select(child_doc.name) + .where( + (child_doc.item_code == self.item_code) + & (child_doc.parent == self.reference_name) + & (child_doc.docstatus < 2) + & (qi_doc.name.isnull()) + ) + .orderby(child_doc.idx) + ).run(pluck=True) - if len(child_row_references) == 1: + if len(child_row_references): 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 frappe.db.get_single_value( @@ -413,7 +387,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( f""" - SELECT item_code + SELECT distinct item_code, item_name, item_group FROM `tab{from_doctype}` WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s {qi_condition} {cond} {mcond} @@ -444,10 +418,11 @@ def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters limit_start=start, limit_page_length=page_len, filters={ - "docstatus": 1, + "docstatus": ("<", 2), "name": ("like", "%%%s%%" % txt), "item_code": filters.get("item_code"), "reference_name": ("in", [filters.get("reference_name", ""), ""]), + "child_row_reference": ("in", [filters.get("child_row_reference", ""), ""]), }, as_list=1, ) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 63597dd3e72..51455ef0d24 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -179,6 +179,7 @@ frappe.ui.form.on("Stock Entry", { inspection_type: "Incoming", reference_type: frm.doc.doctype, reference_name: frm.doc.name, + child_row_reference: row.doc.name, item_code: row.doc.item_code, description: row.doc.description, item_serial_no: row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null, @@ -194,6 +195,7 @@ frappe.ui.form.on("Stock Entry", { filters: { item_code: d.item_code, reference_name: doc.name, + child_row_reference: d.name, }, }; }); diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 11426aea51a..ddef027d0b8 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -192,7 +192,7 @@ class StockLedgerEntry(Document): mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"] for k in mandatory: if not self.get(k): - frappe.throw(_("{0} is required").format(self.meta.get_label(k))) + frappe.throw(_("{0} is required").format(_(self.meta.get_label(k)))) if self.voucher_type != "Stock Reconciliation" and not self.actual_qty: frappe.throw(_("Actual Qty is mandatory")) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index a7fb532f913..fee71460ee5 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -898,10 +898,6 @@ class StockReconciliation(StockController): self.update_inventory_dimensions(row, data) - if self.docstatus == 1 and has_dimensions and (not row.batch_no or not row.serial_and_batch_bundle): - data.qty_after_transaction = data.actual_qty - data.actual_qty = 0.0 - return data def make_sle_on_cancel(self): @@ -1266,12 +1262,12 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code) for d in items: - if d.item_code in itemwise_batch_data: + if (d.item_code, d.warehouse) in itemwise_batch_data: valuation_rate = get_stock_balance( d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True )[1] - for row in itemwise_batch_data.get(d.item_code): + for row in itemwise_batch_data.get((d.item_code, d.warehouse)): if ignore_empty_stock and not row.qty: continue @@ -1403,7 +1399,7 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): columns, data = execute(filters) for row in data: - itemwise_batch_data.setdefault(row[0], []).append( + itemwise_batch_data.setdefault((row[0], row[3]), []).append( frappe._dict( { "item_code": row[0], diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json index cbbb0ce0990..3137bedfee4 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -13,7 +13,6 @@ "end_time", "limits_dont_apply_on", "item_based_reposting", - "do_reposting_for_each_stock_transaction", "errors_notification_section", "notify_reposting_error_to_role" ], @@ -66,18 +65,12 @@ "fieldname": "errors_notification_section", "fieldtype": "Section Break", "label": "Errors Notification" - }, - { - "default": "0", - "fieldname": "do_reposting_for_each_stock_transaction", - "fieldtype": "Check", - "label": "Do reposting for each Stock Transaction" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-04-24 12:19:40.204888", + "modified": "2025-07-08 11:27:46.659056", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reposting Settings", diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index eb3d38bfbfc..50f39817fff 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -16,7 +16,6 @@ class StockRepostingSettings(Document): if TYPE_CHECKING: from frappe.types import DF - do_reposting_for_each_stock_transaction: DF.Check end_time: DF.Time | None item_based_reposting: DF.Check limit_reposting_timeslot: DF.Check @@ -30,10 +29,6 @@ class StockRepostingSettings(Document): def validate(self): self.set_minimum_reposting_time_slot() - def before_save(self): - if self.do_reposting_for_each_stock_transaction: - self.item_based_reposting = 1 - def set_minimum_reposting_time_slot(self): """Ensure that timeslot for reposting is at least 12 hours.""" if not self.limit_reposting_timeslot: diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py index e53659c1735..a6dc72d7a42 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py @@ -38,51 +38,3 @@ class TestStockRepostingSettings(unittest.TestCase): users = get_recipients() self.assertTrue(user in users) - - def test_do_reposting_for_each_stock_transaction(self): - from erpnext.stock.doctype.item.test_item import make_item - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 1) - if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"): - frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0) - - item = make_item( - "_Test item for reposting check for each transaction", properties={"is_stock_item": 1} - ).name - - stock_entry = make_stock_entry( - item_code=item, - qty=1, - rate=100, - stock_entry_type="Material Receipt", - target="_Test Warehouse - _TC", - ) - - riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name") - self.assertTrue(riv) - - frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0) - - def test_do_not_reposting_for_each_stock_transaction(self): - from erpnext.stock.doctype.item.test_item import make_item - from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - frappe.db.set_single_value("Stock Reposting Settings", "do_reposting_for_each_stock_transaction", 0) - if frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"): - frappe.db.set_single_value("Stock Reposting Settings", "item_based_reposting", 0) - - item = make_item( - "_Test item for do not reposting check for each transaction", properties={"is_stock_item": 1} - ).name - - stock_entry = make_stock_entry( - item_code=item, - qty=1, - rate=100, - stock_entry_type="Material Receipt", - target="_Test Warehouse - _TC", - ) - - riv = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name}, pluck="name") - self.assertFalse(riv) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index a66214c0aa7..5d6aa9f68f8 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -115,7 +115,7 @@ class StockReservationEntry(Document): ] for d in mandatory: if not self.get(d): - msg = _("{0} is required").format(self.meta.get_label(d)) + msg = _("{0} is required").format(_(self.meta.get_label(d))) frappe.throw(msg) def validate_group_warehouse(self) -> None: diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index b7283529772..7e40fd681eb 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -65,7 +65,7 @@ class TransactionBase(StatusUpdater): frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name)) for field, condition in fields: - if prevdoc_values[field] is not None and field not in self.exclude_fields: + if prevdoc_values[field] not in [None, ""] and field not in self.exclude_fields: self.validate_value(field, condition, prevdoc_values[field], doc) def get_prev_doc_reference_details(self, reference_names, reference_doctype, fields):