diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json index 4a2584ae1b9..af0aca38c93 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json @@ -37,6 +37,10 @@ "account_type": "Stock", "account_category": "Stock Assets" }, + "Stock Delivered But Not Billed": { + "account_type": "Stock Delivered But Not Billed", + "account_category": "Stock Assets" + }, "account_type": "Stock", "account_category": "Stock Assets" }, @@ -223,10 +227,6 @@ "Stock Received But Not Billed": { "account_type": "Stock Received But Not Billed", "account_category": "Trade Payables" - }, - "Stock Delivered But Not Billed": { - "account_type": "Stock Delivered But Not Billed", - "account_category": "Trade Payables" } }, "Duties and Taxes": { diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index a838e1647d2..de1db2cab82 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -471,6 +471,25 @@ def on_doctype_update(): frappe.db.add_index("GL Entry", ["posting_date", "company"]) frappe.db.add_index("GL Entry", ["party_type", "party"]) + if frappe.db.db_type == "postgres": + # Postgres-only partial/covering indexes for the financial reports (General Ledger, Trial + # Balance, Balance Sheet, P&L), which always filter `is_cancelled = 0` and scope by company. + # `where`/`include` are no-ops on MariaDB and its optimizer ignores these anyway, so they are + # added only on postgres to avoid dead write overhead on this insert-hot table. + frappe.db.add_index( + "GL Entry", + ["company", "posting_date", "account"], + index_name="gle_active_detail", + where="is_cancelled = 0", + ) + frappe.db.add_index( + "GL Entry", + ["company", "account", "posting_date"], + index_name="gle_active_cover", + where="is_cancelled = 0", + include=["debit", "credit"], + ) + def rename_gle_sle_docs(): for doctype in ["GL Entry", "Stock Ledger Entry"]: diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 9713b11bfe7..ffe6630c725 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -29,7 +29,7 @@ frappe.ui.form.on("Journal Entry", { refresh(frm) { if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) { - frm.set_read_only(); + erpnext.journal_entry.lock_reversal_entry(frm); } erpnext.toggle_naming_series(); @@ -232,6 +232,13 @@ Object.assign(erpnext.journal_entry, { } }, + lock_reversal_entry(frm) { + frm.fields + .filter((field) => field.has_input) + .forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1)); + frm.set_df_property("accounts", "read_only", 1); + }, + add_custom_buttons(frm) { if (frm.doc.docstatus > 0) { frm.add_custom_button( diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index f19996dcf80..806a5934940 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -360,12 +360,15 @@ class TestPeriodClosingVoucher(ERPNextTestSuite): self.make_period_closing_voucher(posting_date="2021-03-31") - # Passed posting_date is after PCV end date, so cancellation should not fail. - make_reverse_gl_entries( - voucher_type="Journal Entry", - voucher_no=jv.name, - posting_date="2022-01-01", - ) + frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", "2021-12-31") + + try: + make_reverse_gl_entries( + voucher_type="Journal Entry", + voucher_no=jv.name, + ) + finally: + frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", None) totals_after_cancel = frappe.get_all( "GL Entry", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 5aac9d902e1..c3603290f47 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -13,7 +13,7 @@ {% endif %} -

{{ _("GENERAL LEDGER") }}

+

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

{% if filters.party[0] == filters.party_name[0] %}
{{ _("Customer: ") }} {{ filters.party_name[0] }}
diff --git a/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py b/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py index 2ec0dd335f1..ee9635af5a1 100644 --- a/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py +++ b/erpnext/accounts/doctype/sales_invoice/services/gl_composer.py @@ -33,9 +33,11 @@ class SalesInvoiceGLComposer(BaseGLComposer): self.make_item_gl_entries(gl_entries) - disable_sdbnb_in_sr = frappe.get_cached_value("Company", doc.company, "disable_sdbnb_in_sr") + disable_sdbnb_in_sr, is_sdbnb_enabled = frappe.get_cached_value( + "Company", doc.company, ["disable_sdbnb_in_sr", "enable_stock_delivered_but_not_billed"] + ) - if not (doc.is_return and disable_sdbnb_in_sr): + if is_sdbnb_enabled and not (doc.is_return and disable_sdbnb_in_sr): self.stock_delivered_but_not_billed_gl_entries(gl_entries) self.make_precision_loss_gl_entry(gl_entries) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 93ddefefe85..770b24fe4ca 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1576,14 +1576,14 @@ class TestSalesInvoice(ERPNextTestSuite): frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1) def test_stock_delivered_but_not_billed_gl_on_invoice(self): - company = "_Test Company with perpetual inventory" + company = "_Test SDBNB Company" from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note make_purchase_receipt( company=company, item_code="_Test FG Item", - warehouse="Stores - TCP1", - cost_center="Main - TCP1", + warehouse="Stores - _TSDBNB", + cost_center="Main - _TSDBNB", qty=5, rate=100, ) @@ -1591,13 +1591,13 @@ class TestSalesInvoice(ERPNextTestSuite): dn = create_delivery_note( company=company, item_code="_Test FG Item", - warehouse="Stores - TCP1", - cost_center="Main - TCP1", + warehouse="Stores - _TSDBNB", + cost_center="Main - _TSDBNB", qty=2, rate=300, ) # A perpetual-inventory Delivery Note books the cost to the SDBNB account - self.assertEqual(dn.items[0].expense_account, "Stock Delivered But Not Billed - TCP1") + self.assertEqual(dn.items[0].expense_account, "Stock Delivered But Not Billed - _TSDBNB") si = make_sales_invoice(dn.name) si.insert() @@ -1609,9 +1609,9 @@ class TestSalesInvoice(ERPNextTestSuite): fields=["account", "debit", "credit"], ) sdbnb_credit = sum( - row.credit for row in gl_entries if row.account == "Stock Delivered But Not Billed - TCP1" + row.credit for row in gl_entries if row.account == "Stock Delivered But Not Billed - _TSDBNB" ) - cogs_debit = sum(row.debit for row in gl_entries if row.account == "Cost of Goods Sold - TCP1") + cogs_debit = sum(row.debit for row in gl_entries if row.account == "Cost of Goods Sold - _TSDBNB") # Billing reverses SDBNB and recognises the cost in COGS for an equal amount self.assertTrue(sdbnb_credit > 0) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 1e54eb0370c..be48de11f8f 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -640,13 +640,15 @@ def make_reverse_gl_entries( partial_cancel=partial_cancel, ) validate_accounting_period(gl_entries) - check_freezing_date(gl_entries[0]["posting_date"], gl_entries[0]["company"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) - # For reverse entries, use the posting_date parameter if provided and valid - # Otherwise fall back to original posting_date - validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + if immutable_ledger_enabled: + validation_date = posting_date or frappe.form_dict.get("posting_date") or getdate() + else: + validation_date = posting_date if posting_date else gl_entries[0]["posting_date"] + + check_freezing_date(validation_date, gl_entries[0]["company"], adv_adj) validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"]) if partial_cancel: @@ -715,7 +717,7 @@ def make_reverse_gl_entries( if immutable_ledger_enabled: new_gle["is_cancelled"] = 0 - new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate() + new_gle["posting_date"] = posting_date or frappe.form_dict.get("posting_date") or getdate() elif posting_date: new_gle["posting_date"] = posting_date diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 2fa6ceeb08c..8541e094640 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -174,7 +174,17 @@ frappe.query_reports["Accounts Payable"] = { }, get_datatable_options(options) { - return Object.assign(options, { checkboxColumn: true }); + return Object.assign(options, { + checkboxColumn: true, + events: { + onCheckRow: () => erpnext.accounts.toggle_create_pe_primary_action(frappe.query_report), + }, + }); + }, + + after_refresh: function (report) { + report.datatable?.rowmanager?.checkAll(false); + report.page.clear_primary_action(); }, onload: function (report) { @@ -186,20 +196,27 @@ frappe.query_reports["Accounts Payable"] = { if (frappe.boot.sysdefaults.default_ageing_range) { report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range); } - - if (frappe.model.can_create("Payment Entry")) { - report.page.add_inner_button( - __("Create Payment Entries"), - function () { - erpnext.accounts.create_payment_entries_from_payable_report(report); - }, - __("Actions") - ); - } }, }; frappe.provide("erpnext.accounts"); + +erpnext.accounts.toggle_create_pe_primary_action = function (report) { + if (!report || !report.datatable || !frappe.model.can_create("Payment Entry")) return; + + const has_purchase_invoice = report.datatable.rowmanager + .getCheckedRows() + .some((i) => report.datatable.datamanager.data[i]?.voucher_type === "Purchase Invoice"); + + if (has_purchase_invoice) { + report.page.set_primary_action(__("Create Payment Entries"), () => + erpnext.accounts.create_payment_entries_from_payable_report(report) + ); + } else { + report.page.clear_primary_action(); + } +}; + erpnext.accounts.create_payment_entries_from_payable_report = function (report) { const datatable = report.datatable; if (!datatable) return; diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.json b/erpnext/accounts/report/accounts_payable/accounts_payable.json index 9c713fccf64..5caee4f5c1f 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.json +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 3, "is_standard": "Yes", - "modified": "2026-06-25 12:03:36.559152", + "modified": "2026-07-01 13:37:41.185347", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Payable", @@ -40,6 +40,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/accounts_payable_summary/test_accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/test_accounts_payable_summary.py new file mode 100644 index 00000000000..46491b1ad37 --- /dev/null +++ b/erpnext/accounts/report/accounts_payable_summary/test_accounts_payable_summary.py @@ -0,0 +1,144 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.report.accounts_payable_summary.accounts_payable_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestAccountsPayableSummary(ERPNextTestSuite): + """Payable Summary is a thin wrapper over AccountsReceivableSummary with + account_type=Payable; these tests lock the supplier-side output: invoiced, + advance, paid, outstanding, ageing buckets and the optional GL-balance / + future-payment columns.""" + + def setUp(self): + frappe.set_user("Administrator") + self.maxDiff = None + self.company = "_Test Company" + self.supplier = "_Test Supplier" + + def _filters(self, **overrides): + filters = { + "company": self.company, + "supplier": self.supplier, + "posting_date": today(), + "range": "30, 60, 90, 120", + } + filters.update(overrides) + return filters + + def _make_invoice(self, rate=200): + return make_purchase_invoice( + company=self.company, + supplier=self.supplier, + qty=1, + rate=rate, + price_list_rate=rate, + posting_date=today(), + ) + + def _expected_row(self, pi, **overrides): + supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group") + row = { + "party_type": "Supplier", + "advance": 0, + "party": self.supplier, + "invoiced": 200.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 200.0, + "range1": 200.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 200.0, + "future_amount": 0.0, + "sales_person": [], + "currency": pi.currency, + "supplier_group": supplier_group, + } + row.update(overrides) + return row + + def test_01_payable_summary_output(self): + """Invoiced -> advance -> partial payment progression for a single supplier.""" + filters = self._filters() + pi = self._make_invoice() + + expected = self._expected_row(pi) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # advance payment: pay 50 but allocate nothing against the invoice + pe = get_payment_entry(pi.doctype, pi.name) + pe.paid_amount = 50 + pe.references[0].allocated_amount = 0 + pe.save().submit() + + expected.update({"advance": 50.0, "outstanding": 150.0, "range1": 150.0, "total_due": 150.0}) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # partial payment allocated against the invoice + pe = get_payment_entry(pi.doctype, pi.name) + pe.paid_amount = 125 + pe.references[0].allocated_amount = 125 + pe.save().submit() + + expected.update( + {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0} + ) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + @ERPNextTestSuite.change_settings("Buying Settings", {"supp_master_name": "Naming Series"}) + def test_02_gl_balance_and_future_payment_columns(self): + """Naming-series naming adds party_name; show_gl_balance / show_future_payments + add their columns; a fully-paid invoice drops out of the report.""" + filters = self._filters() + pi = self._make_invoice() + + pe = get_payment_entry(pi.doctype, pi.name) + pe.paid_amount = 150 + pe.references[0].allocated_amount = 150 + pe.save().submit() + + expected = self._expected_row( + pi, + party_name=frappe.db.get_value("Supplier", self.supplier, "supplier_name"), + paid=150.0, + outstanding=50.0, + range1=50.0, + total_due=50.0, + ) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # GL balance reconciliation columns + filters.update({"show_gl_balance": True}) + expected.update({"gl_balance": 50.0, "diff": 0.0}) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # future payment columns + filters.update({"show_future_payments": True}) + expected.update({"remaining_balance": 50.0}) + rows = execute(filters)[1] + self.assertEqual(len(rows), 1) + self.assertDictEqual(rows[0], expected) + + # clear the remaining balance -> supplier drops out of the summary entirely + get_payment_entry(pi.doctype, pi.name).save().submit() + rows = execute(filters)[1] + self.assertEqual(len(rows), 0) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json index dcc3c2c6a49..ef9b6df88d4 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 5, "is_standard": "Yes", - "modified": "2026-06-25 12:03:28.812092", + "modified": "2026-07-01 13:37:44.167999", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Receivable", @@ -34,6 +34,6 @@ "role": "Accounts User" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 756d0c2ebbb..1090b7f4b9c 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -277,7 +277,7 @@ def get_chart_data(filters, chart_columns, asset, liability, equity, currency): return chart -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if not (conn := get_latest_sync("GL Entry")): diff --git a/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py new file mode 100644 index 00000000000..b44c3f987e0 --- /dev/null +++ b/erpnext/accounts/report/bank_clearance_summary/test_bank_clearance_summary.py @@ -0,0 +1,64 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.bank_clearance_summary.bank_clearance_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + +BANK_ACCOUNT = "_Test Bank - _TC" + + +class TestBankClearanceSummary(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "account": BANK_ACCOUNT, + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + } + ) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, payment_entry): + for row in data: + if row[1] == payment_entry: + return row + return None + + def test_uncleared_then_cleared_journal_entry(self): + je = make_journal_entry(BANK_ACCOUNT, "Sales - _TC", 5000, submit=True, posting_date="2026-06-01") + + # Uncleared: the bank row appears with the debit amount and no clearance date + row = self.find_row(self.run_report(), je.name) + self.assertIsNotNone(row, "Journal Entry not listed in Bank Clearance Summary") + self.assertEqual(row[0], "Journal Entry") + self.assertEqual(frappe.utils.getdate(row[2]), frappe.utils.getdate("2026-06-01")) + self.assertIsNone(row[4]) # clearance_date empty -> uncleared + self.assertEqual(row[5], "Sales - _TC") # against account + self.assertEqual(row[6], 5000) # debit - credit on the bank account + + # Cleared: set the clearance date on the Journal Entry and re-run + frappe.db.set_value("Journal Entry", je.name, "clearance_date", "2026-06-05") + + row = self.find_row(self.run_report(), je.name) + self.assertIsNotNone(row) + self.assertEqual(frappe.utils.getdate(row[4]), frappe.utils.getdate("2026-06-05")) + self.assertEqual(row[6], 5000) + + def test_date_filter_excludes_out_of_range_entries(self): + je = make_journal_entry(BANK_ACCOUNT, "Sales - _TC", 3000, submit=True, posting_date="2026-06-10") + + # Within range: present + self.assertIsNotNone(self.find_row(self.run_report(), je.name)) + + # Window entirely after the posting date (from_date lower bound): excluded + after = self.run_report(from_date="2026-07-01", to_date="2026-12-31") + self.assertIsNone(self.find_row(after, je.name)) + + # Window ending before the posting date (to_date upper bound): excluded + before = self.run_report(from_date="2026-01-01", to_date="2026-06-09") + self.assertIsNone(self.find_row(before, je.name)) diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py index dc6192e7544..0ec4652146e 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py @@ -31,7 +31,7 @@ def get_report_filters(report_filters): ] if report_filters.get("purchase_invoice"): - filters.append(["Purchase Invoice", "per_received", "in", [report_filters.get("purchase_invoice")]]) + filters.append(["Purchase Invoice", "name", "=", report_filters.get("purchase_invoice")]) return filters diff --git a/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py new file mode 100644 index 00000000000..aed83005a1a --- /dev/null +++ b/erpnext/accounts/report/billed_items_to_be_received/test_billed_items_to_be_received.py @@ -0,0 +1,102 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import today + +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.report.billed_items_to_be_received.billed_items_to_be_received import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBilledItemsToBeReceived(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "posting_date": today(), + } + ) + filters.update(extra) + return execute(filters)[1] + + def get_rows_for(self, data, pi_name): + return [row for row in data if row.get("name") == pi_name] + + def test_billed_but_not_received_item_appears(self): + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=0, + ) + + rows = self.get_rows_for(self.run_report(), pi.name) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row.get("supplier"), "_Test Supplier") + self.assertEqual(row.get("company"), "_Test Company") + self.assertEqual(row.get("item_code"), "_Test Item") + self.assertEqual(row.get("qty"), 5) + self.assertEqual(row.get("received_qty"), 0) + self.assertEqual(row.get("rate"), 200) + self.assertEqual(row.get("amount"), 1000) + + def test_stock_updating_invoice_is_excluded(self): + """update_stock=1 means the item is already received; it must not appear.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=1, + ) + + rows = self.get_rows_for(self.run_report(), pi.name) + self.assertEqual(len(rows), 0) + + def test_fully_received_invoice_drops_off(self): + """When per_received reaches 100 the invoice is fully received and drops off.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=0, + ) + + # Present while nothing has been received. + self.assertEqual(len(self.get_rows_for(self.run_report(), pi.name)), 1) + + frappe.db.set_value("Purchase Invoice", pi.name, "per_received", 100) + + # Absent once fully received. + self.assertEqual(len(self.get_rows_for(self.run_report(), pi.name)), 0) + + def test_posting_date_upper_bound_filter(self): + """A PI posted after the filter's posting_date must be excluded.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", + item_code="_Test Item", + qty=5, + rate=200, + update_stock=0, + ) + + rows = self.get_rows_for(self.run_report(posting_date="2000-01-01"), pi.name) + self.assertEqual(len(rows), 0) + + def test_purchase_invoice_filter_scopes_to_that_invoice(self): + """The optional purchase_invoice filter must narrow to that invoice only.""" + pi = make_purchase_invoice( + supplier="_Test Supplier", item_code="_Test Item", qty=5, rate=200, update_stock=0 + ) + other = make_purchase_invoice( + supplier="_Test Supplier", item_code="_Test Item", qty=3, rate=200, update_stock=0 + ) + + names = {row.get("name") for row in self.run_report(purchase_invoice=pi.name)} + self.assertEqual(names, {pi.name}) + self.assertNotIn(other.name, names) diff --git a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py index a3a652cb658..e1f2bc5ef0e 100644 --- a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py @@ -2,26 +2,116 @@ # For license information, please see license.txt import frappe +from frappe.utils import nowdate +from erpnext.accounts.doctype.budget.test_budget import make_budget, set_total_expense_zero +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute +from erpnext.accounts.utils import get_fiscal_year from erpnext.tests.utils import ERPNextTestSuite +ACCOUNT = "_Test Account Cost for Goods Sold - _TC" +COST_CENTER = "_Test Cost Center - _TC" +COST_CENTER_2 = "_Test Cost Center 2 - _TC" + class TestBudgetVarianceReport(ERPNextTestSuite): + def setUp(self): + self.fy = get_fiscal_year(nowdate())[0] + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_fiscal_year": self.fy, + "to_fiscal_year": self.fy, + "period": "Yearly", + "budget_against": "Cost Center", + **extra, + } + ) + return execute(filters)[1] + + def report_row(self, data, dimension, account=ACCOUNT): + row = next( + (r for r in data if r["budget_against"] == dimension and r["account"] == account), + None, + ) + self.assertIsNotNone(row, f"No report row for {dimension} / {account}") + return row + + def field(self, label): + return frappe.scrub(f"{label} {self.fy}") + def test_report_executes(self): # Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on # both MariaDB and postgres. - company = frappe.db.get_value("Company", {}, "name") - fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc") columns, *_rest = execute( frappe._dict( { - "company": company, - "from_fiscal_year": fy, - "to_fiscal_year": fy, + "company": "_Test Company", + "from_fiscal_year": self.fy, + "to_fiscal_year": self.fy, "period": "Yearly", "budget_against": "Cost Center", } ) ) self.assertTrue(columns) + + def test_budget_amount_shown_with_zero_actual(self): + # neutralise any committed actuals so the exact Actual/Variance assertions hold + set_total_expense_zero(nowdate(), "cost_center") + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) + + row = self.report_row(self.run_report(), COST_CENTER) + self.assertEqual(row[self.field("Budget")], 120000) + self.assertEqual(row[self.field("Actual")], 0) + self.assertEqual(row[self.field("Variance")], 120000) + + def test_actual_expense_updates_actual_and_variance(self): + # zero out pre-committed actuals: keeps Actual exact and avoids the budget's + # "Stop" action rejecting the journal entry when prior actuals already exist + set_total_expense_zero(nowdate(), "cost_center") + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) + # book an actual expense well within the annual budget so the "Stop" action does not block it + make_journal_entry(ACCOUNT, "_Test Bank - _TC", 50000, cost_center=COST_CENTER, submit=True) + + row = self.report_row(self.run_report(), COST_CENTER) + self.assertEqual(row[self.field("Actual")], 50000) + self.assertEqual(row[self.field("Variance")], 70000) # 120000 - 50000 + + def test_budget_against_filter_limits_dimensions(self): + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER_2, budget_amount=80000, submit_budget=1 + ) + + data = self.run_report(budget_against_filter=[COST_CENTER]) + dimensions = {row["budget_against"] for row in data} + self.assertEqual(dimensions, {COST_CENTER}) + + def test_monthly_period_totals(self): + # zero out pre-committed actuals so total_actual reflects only this test's entry + set_total_expense_zero(nowdate(), "cost_center") + make_budget( + budget_against="Cost Center", cost_center=COST_CENTER, budget_amount=120000, submit_budget=1 + ) + make_journal_entry(ACCOUNT, "_Test Bank - _TC", 50000, cost_center=COST_CENTER, submit=True) + + row = self.report_row(self.run_report(period="Monthly"), COST_CENTER) + # totals roll up the per-month columns across the year + self.assertEqual(row["total_budget"], 120000) + self.assertEqual(row["total_actual"], 50000) + self.assertEqual(row["total_variance"], 70000) + + def test_no_budget_returns_no_rows(self): + # a dimension without any budget produces no report rows + data = self.run_report(budget_against_filter=["_Test Write Off Cost Center - _TC"]) + self.assertEqual(data, []) diff --git a/erpnext/accounts/report/calculated_discount_mismatch/test_calculated_discount_mismatch.py b/erpnext/accounts/report/calculated_discount_mismatch/test_calculated_discount_mismatch.py new file mode 100644 index 00000000000..dd4386d4afd --- /dev/null +++ b/erpnext/accounts/report/calculated_discount_mismatch/test_calculated_discount_mismatch.py @@ -0,0 +1,82 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import json + +import frappe +from frappe.utils.formatters import format_value + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.calculated_discount_mismatch.calculated_discount_mismatch import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCalculatedDiscountMismatch(ERPNextTestSuite): + """Integrity detector: flag transactions whose stored ``discount_amount`` was tampered + after the fact (a Version records the change) while ``additional_discount_percentage`` + stayed the same, so the stored amount no longer matches the percentage-derived value. + """ + + def run_report(self, docname: str) -> dict | None: + """Run the (filter-less) report and return the row for ``docname``, if any.""" + _columns, data = execute(frappe._dict({})) + return next((row for row in data if row["docname"] == docname), None) + + def create_discounted_invoice(self) -> "frappe.Document": + """Draft Sales Invoice (rate 1000) with a 10% additional discount. + + The controller derives ``discount_amount`` = 10% of the grand total = 100.00, + so the stored amount is consistent with the percentage. + """ + invoice = create_sales_invoice(rate=1000, qty=1, do_not_submit=1) + invoice.additional_discount_percentage = 10 + invoice.save() + invoice.reload() + return invoice + + def test_consistent_discount_is_not_flagged(self): + """A submitted invoice whose discount_amount matches its percentage is not reported.""" + invoice = self.create_discounted_invoice() + invoice.submit() + invoice.reload() + + self.assertEqual(invoice.discount_amount, 100.0) + self.assertIsNone(self.run_report(invoice.name)) + + def test_tampered_discount_is_flagged(self): + """Directly overwriting discount_amount (leaving the percentage intact) is reported. + + This reproduces the real-world integrity breach: a Version records the + ``discount_amount`` change, its ``new`` value equals the current stored amount, and + ``additional_discount_percentage`` was not touched -- exactly the shape the report + queries for. + """ + invoice = self.create_discounted_invoice() + consistent_amount = invoice.discount_amount # 100.00, matches the 10% percentage + tampered_amount = 250.0 + + discount_field = frappe.get_meta("Sales Invoice").get_field("discount_amount") + # Format exactly as the report does so version.new == format_value(current amount). + suspected = format_value(consistent_amount, df=discount_field, currency=invoice.currency) + actual = format_value(tampered_amount, df=discount_field, currency=invoice.currency) + + # Tamper the stored amount directly, bypassing the controller that would recompute it. + frappe.db.set_value("Sales Invoice", invoice.name, "discount_amount", tampered_amount) + self.record_discount_change(invoice.name, suspected, actual) + + row = self.run_report(invoice.name) + + self.assertIsNotNone(row) + self.assertEqual(row["doctype"], "Sales Invoice") + self.assertEqual(row["actual_discount_percentage"], 10.0) + self.assertEqual(row["actual_discount_amount"], actual) + self.assertEqual(row["suspected_discount_amount"], suspected) + + def record_discount_change(self, docname: str, old: str, new: str) -> None: + """Insert the Version audit row a direct discount_amount edit would have produced.""" + version = frappe.new_doc("Version") + version.ref_doctype = "Sales Invoice" + version.docname = docname + version.data = json.dumps({"changed": [["discount_amount", old, new]]}, separators=(",", ":")) + version.flags.ignore_version = True + version.insert(ignore_permissions=True) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index fba7054e0a7..237f37e767f 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -582,7 +582,12 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com total += flt(row[company]) row["has_value"] = has_value - row["total"] = total + # when accumulating into the group company, that company's column already consolidates its + # descendants, so summing every company column would double-count; use the group total directly. + if filters.get("accumulated_in_group_company"): + row["total"] = flt(row.get(filters.company, 0.0), 3) + else: + row["total"] = total data.append(row) diff --git a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py new file mode 100644 index 00000000000..1fb6a68e3b6 --- /dev/null +++ b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py @@ -0,0 +1,129 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt, today + +from erpnext.accounts.report.consolidated_financial_statement.consolidated_financial_statement import ( + execute, +) +from erpnext.accounts.utils import get_fiscal_year +from erpnext.tests.utils import ERPNextTestSuite + +PARENT_COMPANY = "Parent Group Company India" +CHILD_COMPANY = "Child Company India" + + +class TestConsolidatedFinancialStatement(ERPNextTestSuite): + """Consolidation is exercised via the bootstrap group of companies + (`Parent Group Company India` with child `Child Company India`). Income and + expense posted in the child company must surface in the report that is run + for the parent (group) company.""" + + def setUp(self): + self.fiscal_year = get_fiscal_year(today(), company=PARENT_COMPANY)[0] + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": PARENT_COMPANY, + "filter_based_on": "Fiscal Year", + "from_fiscal_year": self.fiscal_year, + "to_fiscal_year": self.fiscal_year, + "periodicity": "Yearly", + "include_default_book_entries": 1, + } + ) + filters.update(extra) + return execute(filters)[1] + + def post_journal_entry(self, debit_account, credit_account, amount): + je = frappe.new_doc("Journal Entry") + je.posting_date = today() + je.company = CHILD_COMPANY + je.set( + "accounts", + [ + {"account": debit_account, "debit_in_account_currency": amount}, + {"account": credit_account, "credit_in_account_currency": amount}, + ], + ) + je.save() + je.submit() + return je + + def get_row(self, data, account_name_fragment, last_match=False): + """Return the first (or last) row whose account_name contains the fragment. + + Pass ``last_match=True`` to get the leaf/most-specific match when the fragment + is also a prefix of a parent group account (parents precede children in tree order). + """ + found = None + for row in data: + if account_name_fragment in str(row.get("account_name") or ""): + if not last_match: + return row + found = row + return found + + def test_profit_and_loss_reflects_child_company_income(self): + amount = 7000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) + + self.assertTrue(data, "Report returned no rows") + + # child's Sales account is mapped onto the parent chart (Sales - PGCI) + sales_row = self.get_row(data, "Sales", last_match=True) + self.assertIsNotNone(sales_row, "Sales row missing from consolidated P&L") + # >= so a pre-existing Sales balance in the fiscal year doesn't make this brittle + self.assertGreaterEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + + total_income_row = self.get_row(data, "Total Income (Credit)") + self.assertIsNotNone(total_income_row, "Total Income row missing") + self.assertGreaterEqual(flt(total_income_row.get("total")), amount) + + def test_profit_and_loss_reflects_child_company_expense(self): + amount = 3000 + self.post_journal_entry("Marketing Expenses - CCI", "Cash - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) + + expense_row = self.get_row(data, "Marketing Expenses", last_match=True) + self.assertIsNotNone(expense_row, "Marketing Expenses row missing from consolidated P&L") + self.assertGreaterEqual(flt(expense_row.get(CHILD_COMPANY)), amount) + + total_expense_row = self.get_row(data, "Total Expense (Debit)") + self.assertIsNotNone(total_expense_row, "Total Expense row missing") + self.assertGreaterEqual(flt(total_expense_row.get("total")), amount) + + def test_accumulated_in_group_company_rolls_up_to_parent(self): + """With `accumulated_in_group_company`, the child's amount is also + accumulated into the parent company column.""" + amount = 5000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=1) + + sales_row = self.get_row(data, "Sales", last_match=True) + self.assertIsNotNone(sales_row) + child_value = flt(sales_row.get(CHILD_COMPANY)) + self.assertGreaterEqual(child_value, amount) + # parent column picks up the child value when accumulated + self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), child_value) + # the total equals the consolidated (group) value, not the sum of parent + child + # columns -- this is the regression guard for the double-count fix + self.assertEqual(flt(sales_row.get("total")), child_value) + + def test_balance_sheet_executes_and_returns_rows(self): + # posting income leaves a balancing entry in the child's Cash (Asset) account + amount = 4000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Balance Sheet", accumulated_in_group_company=0) + + self.assertTrue(data, "Balance Sheet returned no rows") + cash_row = self.get_row(data, "Cash") + self.assertIsNotNone(cash_row, "Cash asset row missing from consolidated Balance Sheet") + self.assertGreaterEqual(flt(cash_row.get(CHILD_COMPANY)), amount) diff --git a/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py b/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py new file mode 100644 index 00000000000..5d981b77c38 --- /dev/null +++ b/erpnext/accounts/report/custom_financial_statement/test_custom_financial_statement.py @@ -0,0 +1,94 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.custom_financial_statement.custom_financial_statement import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCustomFinancialStatement(ERPNextTestSuite): + """The report renders a Financial Report Template through FinancialReportEngine. + These tests exercise its own entry point: a template with an account-data row + and a calculated row, and the guard that returns nothing without a template.""" + + def setUp(self): + frappe.set_user("Administrator") + self.company = "_Test Company" + self.expense_account = "_Test Account Cost for Goods Sold - _TC" + self.cash_account = "Cash - _TC" + + def _make_template(self): + # rows filter by exact account name so the value is isolated from other data + template_name = f"Test Custom FS {frappe.generate_hash()[:8]}" + return frappe.get_doc( + { + "doctype": "Financial Report Template", + "template_name": template_name, + "report_type": "Profit and Loss Statement", + "rows": [ + { + "reference_code": "EXP", + "display_name": "Test Expense", + "indentation_level": 0, + "data_source": "Account Data", + "balance_type": "Closing Balance", + "calculation_formula": f'["name", "=", "{self.expense_account}"]', + }, + { + "reference_code": "EXP_X2", + "display_name": "Expense Doubled", + "indentation_level": 0, + "data_source": "Calculated Amount", + "calculation_formula": "EXP * 2", + }, + ], + } + ).insert() + + def _filters(self, template_name): + return frappe._dict( + { + "company": self.company, + "report_template": template_name, + "from_fiscal_year": "2024", + "to_fiscal_year": "2024", + "period_start_date": "2024-01-01", + "period_end_date": "2024-12-31", + "filter_based_on": "Date Range", + "periodicity": "Yearly", + "accumulated_values": 0, + } + ) + + def test_account_and_calculated_rows(self): + make_journal_entry( + self.expense_account, + self.cash_account, + 2000, + posting_date="2024-06-15", + company=self.company, + submit=True, + ) + template = self._make_template() + + columns, data = execute(self._filters(template.template_name))[:2] + self.assertTrue(columns) + + rows = {row.get("account_name"): row for row in data} + self.assertIn("Test Expense", rows) + self.assertIn("Expense Doubled", rows) + + period_keys = rows["Test Expense"].get("_segment_info", {}).get("period_keys", []) + self.assertTrue(period_keys, "expected at least one period key in _segment_info") + period_key = period_keys[0] + + # the account-data row picks up the posted expense; the calculated row doubles it + self.assertEqual(flt(rows["Test Expense"][period_key]), 2000.0) + self.assertEqual(flt(rows["Expense Doubled"][period_key]), 4000.0) + + def test_no_template_returns_nothing(self): + """Without a report_template the report short-circuits and returns None.""" + self.assertIsNone(execute(frappe._dict({"company": self.company}))) diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/test_delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/test_delivered_items_to_be_billed.py new file mode 100644 index 00000000000..acdce19c105 --- /dev/null +++ b/erpnext/accounts/report/delivered_items_to_be_billed/test_delivered_items_to_be_billed.py @@ -0,0 +1,89 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.delivered_items_to_be_billed.delivered_items_to_be_billed import execute +from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestDeliveredItemsToBeBilled(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "posting_date": "2026-06-30", + } + ) + filters.update(extra) + return execute(filters)[1] + + def stock_up_item(self): + make_stock_entry( + item_code="_Test Item", + target="Stores - _TC", + qty=20, + basic_rate=100, + posting_date="2026-05-25", + ) + + def test_unbilled_delivery_note_appears(self): + self.stock_up_item() + dn = create_delivery_note( + item_code="_Test Item", + warehouse="Stores - _TC", + qty=5, + rate=300, + customer="_Test Customer", + posting_date="2026-06-01", + ) + + rows = self.run_report(delivery_note=dn.name) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row.name, dn.name) + self.assertEqual(row.customer, "_Test Customer") + self.assertEqual(row.item_code, "_Test Item") + self.assertEqual(row.amount, 1500) + self.assertEqual(row.billed_amount, 0) + self.assertEqual(row.returned_amount, 0) + self.assertEqual(row.pending_amount, 1500) + + def test_fully_billed_delivery_note_drops_out(self): + self.stock_up_item() + dn = create_delivery_note( + item_code="_Test Item", + warehouse="Stores - _TC", + qty=5, + rate=300, + customer="_Test Customer", + posting_date="2026-06-01", + ) + + self.assertEqual(len(self.run_report(delivery_note=dn.name)), 1) + + si = make_sales_invoice(dn.name) + si.posting_date = "2026-06-02" + si.set_posting_time = 1 + si.insert() + si.submit() + + self.assertEqual(self.run_report(delivery_note=dn.name), []) + + def test_date_filter_excludes_later_delivery_notes(self): + self.stock_up_item() + dn = create_delivery_note( + item_code="_Test Item", + warehouse="Stores - _TC", + qty=5, + rate=300, + customer="_Test Customer", + posting_date="2026-07-15", + ) + + rows = self.run_report(delivery_note=dn.name, posting_date="2026-06-30") + self.assertEqual(rows, []) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/test_dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/test_dimension_wise_accounts_balance_report.py new file mode 100644 index 00000000000..fb87342da66 --- /dev/null +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/test_dimension_wise_accounts_balance_report.py @@ -0,0 +1,82 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import today + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.dimension_wise_accounts_balance_report.dimension_wise_accounts_balance_report import ( + execute, +) +from erpnext.accounts.utils import get_fiscal_year +from erpnext.tests.utils import ERPNextTestSuite + + +class TestDimensionWiseAccountsBalance(ERPNextTestSuite): + """Balances accounts one column per value of an accounting dimension (here + Cost Center). Locks the two behaviours that matter: an entry lands in its + own dimension column as debit - credit, and children roll up into parents.""" + + def setUp(self): + frappe.set_user("Administrator") + self.company = "_Test Company" + self.expense_account = "_Test Account Cost for Goods Sold - _TC" + self.cash_account = "Cash - _TC" + + def _make_cost_center(self, name): + full_name = f"{name} - _TC" + if not frappe.db.exists("Cost Center", full_name): + frappe.get_doc( + { + "doctype": "Cost Center", + "cost_center_name": name, + "parent_cost_center": "_Test Company - _TC", + "company": self.company, + "is_group": 0, + } + ).insert() + return full_name + + def _filters(self, **overrides): + filters = frappe._dict( + { + "company": self.company, + "dimension": "Cost Center", + "fiscal_year": get_fiscal_year(today(), company=self.company)[0], + } + ) + filters.update(overrides) + return filters + + def test_dimension_column_and_rollup(self): + # a dedicated cost center isolates our column from any other posted data + cost_center = self._make_cost_center("Test Dimension CC") + make_journal_entry( + self.expense_account, + self.cash_account, + 300, + cost_center=cost_center, + posting_date=today(), + submit=True, + ) + + columns, data = execute(self._filters()) + column = frappe.scrub(cost_center) + self.assertIn(column, [c["fieldname"] for c in columns]) + + rows = {row["account"]: row for row in data} + + # the entry shows as debit - credit under its own dimension column + self.assertEqual(rows[self.expense_account][column], 300.0) + self.assertEqual(rows[self.cash_account][column], -300.0) + + # and rolls up into each account's parent (isolated to our cost center) + expense_parent = frappe.db.get_value("Account", self.expense_account, "parent_account") + cash_parent = frappe.db.get_value("Account", self.cash_account, "parent_account") + self.assertEqual(rows[expense_parent][column], 300.0) + self.assertEqual(rows[cash_parent][column], -300.0) + + def test_requires_fiscal_year(self): + filters = self._filters() + filters.pop("fiscal_year") + self.assertRaises(frappe.ValidationError, execute, filters) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 083f7b62ae8..a702e606edd 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-22 13:38:35.057216", + "modified": "2026-07-01 13:36:06.682661", "modified_by": "Administrator", "module": "Accounts", "name": "General Ledger", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 43383cf6b36..ca06baf4a08 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -820,7 +820,7 @@ def get_columns(filters): return columns -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if conn := get_latest_sync("GL Entry"): diff --git a/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py new file mode 100644 index 00000000000..d7a46d778b3 --- /dev/null +++ b/erpnext/accounts/report/gross_and_net_profit_report/test_gross_and_net_profit_report.py @@ -0,0 +1,85 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.gross_and_net_profit_report.gross_and_net_profit_report import execute +from erpnext.tests.utils import ERPNextTestSuite + +BANK = "_Test Bank - _TC" +INCOME_PARENT = "Income - _TC" +EXPENSE_PARENT = "Expenses - _TC" +# bootstrap leaf accounts that already have include_in_gross = 0 (no creation needed) +NON_GROSS_INCOME = "_Test Account Sales - _TC" +NON_GROSS_EXPENSE = "_Test Account Cost for Goods Sold - _TC" +# an isolated fiscal year so other accounts contribute nothing to the totals +FY = "_Test Fiscal Year 2049" +DATE = "2049-06-01" + + +class TestGrossAndNetProfitReport(ERPNextTestSuite): + def run_report(self, from_fiscal_year=FY, to_fiscal_year=FY): + filters = frappe._dict( + { + "company": "_Test Company", + "filter_based_on": "Fiscal Year", + "from_fiscal_year": from_fiscal_year, + "to_fiscal_year": to_fiscal_year, + "period_start_date": "2049-01-01", + "period_end_date": "2049-12-31", + "periodicity": "Yearly", + "accumulated_values": 0, + "presentation_currency": None, + } + ) + return execute(filters)[1] + + def make_account(self, name, parent, include_in_gross): + account = create_account(account_name=name, parent_account=parent, company="_Test Company") + frappe.db.set_value("Account", account, "include_in_gross", include_in_gross) + return account + + def book_income(self, account, amount): + make_journal_entry(BANK, account, amount, posting_date=DATE, submit=True) + + def book_expense(self, account, amount): + make_journal_entry(account, BANK, amount, posting_date=DATE, submit=True) + + def report_row(self, data, account): + return next(row for row in data if row.get("account") == account) + + def test_gross_profit_excludes_non_gross_accounts(self): + # reuse bootstrap accounts for the non-gross (include_in_gross = 0) side + gross_income = self.make_account("_Test GNP Gross Income", INCOME_PARENT, include_in_gross=1) + gross_expense = self.make_account("_Test GNP Gross Expense", EXPENSE_PARENT, include_in_gross=1) + + self.book_income(gross_income, 10000) + self.book_income(NON_GROSS_INCOME, 2000) + self.book_expense(gross_expense, 4000) + self.book_expense(NON_GROSS_EXPENSE, 1000) + + data = self.run_report() + # gross profit only counts include_in_gross accounts: 10000 - 4000 + self.assertEqual(self.report_row(data, "'Gross Profit'")["total"], 6000) + # net profit counts everything: (10000 + 2000) - (4000 + 1000) + self.assertEqual(self.report_row(data, "'Net Profit'")["total"], 7000) + + def test_net_profit_equals_gross_when_all_included(self): + income = self.make_account("_Test GNP All Income", INCOME_PARENT, include_in_gross=1) + expense = self.make_account("_Test GNP All Expense", EXPENSE_PARENT, include_in_gross=1) + + self.book_income(income, 9000) + self.book_expense(expense, 5000) + + data = self.run_report() + self.assertEqual(self.report_row(data, "'Gross Profit'")["total"], 4000) + self.assertEqual(self.report_row(data, "'Net Profit'")["total"], 4000) + + def test_nothing_included_in_gross_when_no_entries(self): + # a fiscal year with no income/expense entries yields the placeholder row + data = self.run_report( + from_fiscal_year="_Test Fiscal Year 2048", to_fiscal_year="_Test Fiscal Year 2048" + ) + self.assertEqual(data[0]["account"], "'Nothing is included in gross'") diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py index 33fda705cf2..2ba0a7fb4ce 100644 --- a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -84,7 +84,8 @@ def build_query_filters(filters: dict | None = None) -> list: qb_filters = [] if filters: if filters.account: - qb_filters.append(qb.Field("account").isin(filters.account)) + accounts = filters.account if isinstance(filters.account, list | tuple) else [filters.account] + qb_filters.append(qb.Field("account").isin(accounts)) if filters.voucher_no: qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no)) diff --git a/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py new file mode 100644 index 00000000000..9a193dc0779 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/test_invalid_ledger_entries.py @@ -0,0 +1,152 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe import qb + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.invalid_ledger_entries.invalid_ledger_entries import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestInvalidLedgerEntries(ERPNextTestSuite): + """Tests for the Invalid Ledger Entries integrity report. + + The report flags vouchers that still have *active* ledger entries + (GL Entry with is_cancelled=0 or Payment Ledger Entry with delinked=0) + in the given period, but whose source voucher document is no longer + submitted (docstatus != 1). Such orphaned ledgers indicate corruption. + """ + + def setUp(self): + self.company = "_Test Company" + self.debit_account = "_Test Bank - _TC" + self.credit_account = "_Test Cash - _TC" + self.from_date = "2026-01-01" + self.to_date = "2026-12-31" + self.posting_date = "2026-06-01" + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": self.company, + "from_date": self.from_date, + "to_date": self.to_date, + } + ) + filters.update(extra) + return execute(filters)[1] + + def make_submitted_jv(self): + return make_journal_entry( + self.debit_account, + self.credit_account, + amount=500, + posting_date=self.posting_date, + company=self.company, + submit=True, + ) + + def test_healthy_voucher_not_flagged(self): + """A normal balanced, submitted Journal Entry must NOT be flagged.""" + jv = self.make_submitted_jv() + + # It genuinely posted active GL entries, so it is in scope of the scan. + self.assertTrue( + frappe.db.exists( + "GL Entry", + {"voucher_no": jv.name, "is_cancelled": 0, "company": self.company}, + ) + ) + + flagged = {row.get("voucher_no") for row in self.run_report()} + self.assertNotIn(jv.name, flagged) + + def test_orphaned_gl_entries_flagged(self): + """A voucher whose document was set non-submitted while its GL entries + remain active (is_cancelled=0) must be flagged as invalid.""" + jv = self.make_submitted_jv() + + # Corrupt the state: mark the source document as cancelled (docstatus=2) + # without cancelling/removing its GL Entries. This is the exact orphaned + # ledger condition the report detects. + frappe.db.set_value("Journal Entry", jv.name, "docstatus", 2, update_modified=False) + + data = self.run_report() + + matching = [ + row + for row in data + if row.get("voucher_no") == jv.name and row.get("voucher_type") == "Journal Entry" + ] + self.assertEqual(len(matching), 1, "Orphaned voucher should be flagged exactly once") + self.assertEqual(matching[0]["voucher_type"], "Journal Entry") + self.assertEqual(matching[0]["voucher_no"], jv.name) + + def test_voucher_no_filter_scopes_scan(self): + """The voucher_no filter must restrict the scan to that voucher only.""" + orphan = self.make_submitted_jv() + other = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + frappe.db.set_value("Journal Entry", other.name, "docstatus", 2, update_modified=False) + + flagged = {row.get("voucher_no") for row in self.run_report(voucher_no=orphan.name)} + self.assertIn(orphan.name, flagged) + self.assertNotIn(other.name, flagged) + + def test_account_filter_scopes_scan(self): + """The account filter (a MultiSelectList, so a list) must restrict the + scan to vouchers touching one of the given accounts.""" + orphan = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + + # Filtering on an account the voucher touches -> flagged. + flagged = {row.get("voucher_no") for row in self.run_report(account=[self.debit_account])} + self.assertIn(orphan.name, flagged) + + # Filtering on an unrelated account -> not in scope. + unrelated = "Creditors - _TC" + flagged = {row.get("voucher_no") for row in self.run_report(account=[unrelated])} + self.assertNotIn(orphan.name, flagged) + + def test_account_filter_accepts_a_scalar(self): + """A scalar (non-list) account filter must not crash the query.""" + orphan = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + + flagged = {row.get("voucher_no") for row in self.run_report(account=self.debit_account)} + self.assertIn(orphan.name, flagged) + + def test_period_filter_excludes_out_of_range(self): + """Vouchers posted outside the from/to window must not be scanned.""" + orphan = self.make_submitted_jv() + frappe.db.set_value("Journal Entry", orphan.name, "docstatus", 2, update_modified=False) + + flagged = { + row.get("voucher_no") for row in self.run_report(from_date="2025-01-01", to_date="2025-12-31") + } + self.assertNotIn(orphan.name, flagged) + + def test_cancelled_gl_entries_not_flagged(self): + """If the ledger entries are properly cancelled (is_cancelled=1), the + voucher is out of scope even when its document is non-submitted.""" + jv = self.make_submitted_jv() + + gle = qb.DocType("GL Entry") + qb.update(gle).set(gle.is_cancelled, 1).where(gle.voucher_no == jv.name).run() + frappe.db.set_value("Journal Entry", jv.name, "docstatus", 2, update_modified=False) + + flagged = {row.get("voucher_no") for row in self.run_report()} + self.assertNotIn(jv.name, flagged) + + def test_missing_filters_raises(self): + """validate_filters must guard mandatory inputs.""" + self.assertRaises(frappe.ValidationError, execute, None) + + bad = frappe._dict({"from_date": self.from_date, "to_date": self.to_date}) + self.assertRaises(frappe.ValidationError, execute, bad) + + reversed_dates = frappe._dict( + {"company": self.company, "from_date": self.to_date, "to_date": self.from_date} + ) + self.assertRaises(frappe.ValidationError, execute, reversed_dates) diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py index da3fa0762aa..47ada82755e 100644 --- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py @@ -21,6 +21,13 @@ def execute(filters=None): entries = get_entries(filters) invoice_details = get_invoice_posting_date_map(filters) + # Only four range columns are defined (range1-range4, the last being "90 Above"). + # Three thresholds yield exactly four buckets, so payments more than 90 days after + # the invoice land in range4 instead of an unread range5. + report_filters = frappe._dict(filters) + report_filters.range = "30, 60, 90" + report = ReceivablePayableReport(report_filters) + data = [] for d in entries: invoice = invoice_details.get(d.against_voucher_no) or frappe._dict() @@ -29,7 +36,9 @@ def execute(filters=None): d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount}) if d.against_voucher_no: - ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d) + # age the payment by how long after the invoice it was made (payment date - invoice date) + report.age_as_on = getdate(d.posting_date) + report.get_ageing_data(invoice.posting_date, d) row = [ d.voucher_type, diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py new file mode 100644 index 00000000000..f7c4f874c25 --- /dev/null +++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/test_payment_period_based_on_invoice_date.py @@ -0,0 +1,140 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import getdate + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.payment_period_based_on_invoice_date.payment_period_based_on_invoice_date import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestPaymentPeriodBasedOnInvoiceDate(ERPNextTestSuite): + """Depth tests for the Payment Period Based On Invoice Date report. + + The report lists Payment Ledger Entries against invoices and buckets the paid + amount by the payment period -- how long after the invoice the payment was made + (payment date - invoice date) -- into ranges: range1 (0-30), range2 (30-60), + range3 (60-90), range4 (90 Above). + """ + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "payment_type": "Incoming", + "party_type": "Customer", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + } + ) + filters.update(extra) + columns, data = execute(filters) + fieldnames = [c["fieldname"] for c in columns] + # Map each positional row to a dict keyed by column fieldname so assertions + # stay correct even if a column is inserted or reordered. + return columns, [dict(zip(fieldnames, row, strict=False)) for row in data] + + def find_payment_row(self, data, payment_name): + for row in data: + if row["payment_entry"] == payment_name: + return row + return None + + def pay_invoice(self, invoice, payment_date): + pe = get_payment_entry("Sales Invoice", invoice.name) + pe.posting_date = payment_date + pe.reference_no = "1" + pe.reference_date = payment_date + pe.submit() + return pe + + def test_paid_amount_lands_in_0_30_bucket(self): + # invoice 2026-06-01, paid 2026-06-20 -> 19 days after -> 0-30 bucket + invoice = create_sales_invoice(customer="_Test Customer", rate=1000, posting_date="2026-06-01") + payment = self.pay_invoice(invoice, "2026-06-20") + + _columns, data = self.run_report() + + row = self.find_payment_row(data, payment.name) + self.assertIsNotNone(row, "Payment row not found in report output") + + self.assertEqual(row["party_type"], "Customer") + self.assertEqual(row["posting_date"], getdate("2026-06-20")) + self.assertEqual(row["invoice"], invoice.name) + self.assertEqual(row["invoice_posting_date"], getdate("2026-06-01")) + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["age"], 19) # age = payment date - invoice date + + # Buckets: 0-30 filled, others empty. + self.assertEqual(row["range1"], 1000) # 0-30 + self.assertEqual(row["range2"], 0) # 30-60 + self.assertEqual(row["range3"], 0) # 60-90 + self.assertEqual(row["range4"], 0) # 90 Above + + def test_paid_amount_lands_in_30_60_bucket(self): + # invoice 2026-06-01, paid 2026-07-16 -> 45 days after -> 30-60 bucket + invoice = create_sales_invoice(customer="_Test Customer 1", rate=1000, posting_date="2026-06-01") + payment = self.pay_invoice(invoice, "2026-07-16") + + _columns, data = self.run_report() + + row = self.find_payment_row(data, payment.name) + self.assertIsNotNone(row, "Payment row not found in report output") + + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["age"], 45) + # Buckets: 30-60 filled, others empty. + self.assertEqual(row["range1"], 0) + self.assertEqual(row["range2"], 1000) + self.assertEqual(row["range3"], 0) + self.assertEqual(row["range4"], 0) + + def test_payment_over_90_days_lands_in_90_above_bucket(self): + # invoice 2026-01-01, paid 2026-06-01 -> 151 days after -> "90 Above" bucket. + # Regression guard: with four range columns, a payment older than the last + # threshold must fall into range4 rather than an unread range5 (showing 0). + invoice = create_sales_invoice(customer="_Test Customer 2", rate=1000, posting_date="2026-01-01") + payment = self.pay_invoice(invoice, "2026-06-01") + + _columns, data = self.run_report() + + row = self.find_payment_row(data, payment.name) + self.assertIsNotNone(row, "Payment row not found in report output") + + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["age"], 151) + self.assertEqual(row["range1"], 0) + self.assertEqual(row["range2"], 0) + self.assertEqual(row["range3"], 0) + self.assertEqual(row["range4"], 1000) # 90 Above captures the full amount + + def test_columns_expose_expected_age_buckets(self): + columns, _data = self.run_report() + labels_by_fieldname = {c["fieldname"]: c["label"] for c in columns} + self.assertEqual(labels_by_fieldname["range1"], "0-30") + self.assertEqual(labels_by_fieldname["range2"], "30-60") + self.assertEqual(labels_by_fieldname["range3"], "60-90") + self.assertEqual(labels_by_fieldname["range4"], "90 Above") + # Sales Invoice link for Incoming payments. + invoice_col = next(c for c in columns if c["fieldname"] == "invoice") + self.assertEqual(invoice_col["options"], "Sales Invoice") + + def test_invalid_payment_type_party_type_combo_throws(self): + # Incoming + Supplier is invalid. + self.assertRaises( + frappe.ValidationError, + self.run_report, + payment_type="Incoming", + party_type="Supplier", + ) + # Outgoing + Customer is invalid. + self.assertRaises( + frappe.ValidationError, + self.run_report, + payment_type="Outgoing", + party_type="Customer", + ) diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json index 9aa088aefe0..5ddd3af7aa4 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 2, "is_standard": "Yes", - "modified": "2026-06-22 13:38:15.898375", + "modified": "2026-07-01 13:36:14.934965", "modified_by": "Administrator", "module": "Accounts", "name": "Profit and Loss Statement", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 297aa961058..25eca6f4c79 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -207,7 +207,7 @@ def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, cur return chart -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if not (conn := get_latest_sync("GL Entry")): diff --git a/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py new file mode 100644 index 00000000000..e9c98f75821 --- /dev/null +++ b/erpnext/accounts/report/profitability_analysis/test_profitability_analysis.py @@ -0,0 +1,107 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.profitability_analysis.profitability_analysis import execute +from erpnext.tests.utils import ERPNextTestSuite + +INCOME = "Sales - _TC" +EXPENSE = "_Test Account Cost for Goods Sold - _TC" +BANK = "_Test Bank - _TC" + + +class TestProfitabilityAnalysis(ERPNextTestSuite): + def run_report(self, fiscal_year="_Test Fiscal Year 2026", **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "based_on": "Cost Center", + "fiscal_year": fiscal_year, + "from_date": "2026-01-01", + "to_date": "2026-12-31", + **extra, + } + ) + return execute(filters)[1] + + def make_cc(self, name, **args): + create_cost_center(cost_center_name=name, **args) + return name + " - _TC" + + def row(self, data, account): + return next(r for r in data if r.get("account") == account) + + def book_income(self, cost_center, amount, posting_date="2026-06-01"): + create_sales_invoice( + cost_center=cost_center, income_account=INCOME, rate=amount, qty=1, posting_date=posting_date + ) + + def book_expense(self, cost_center, amount, posting_date="2026-06-01"): + make_journal_entry( + EXPENSE, BANK, amount, cost_center=cost_center, posting_date=posting_date, submit=True + ) + + def test_income_expense_and_gross_profit(self): + # a dedicated leaf cost center keeps these exact assertions free of GL that + # other tests may book against a shared cost center in the same fiscal year + cc = self.make_cc("_Test PA Income Expense") + self.book_income(cc, 10000) + self.book_expense(cc, 4000) + + row = self.row(self.run_report(), cc) + self.assertEqual(row["income"], 10000) + self.assertEqual(row["expense"], 4000) + self.assertEqual(row["gross_profit_loss"], 6000) + + def test_parent_cost_center_accumulates_children(self): + parent = self.make_cc("_Test PA Parent", is_group=1) + child_1 = self.make_cc("_Test PA Child 1", parent_cost_center=parent) + child_2 = self.make_cc("_Test PA Child 2", parent_cost_center=parent) + + self.book_income(child_1, 10000) + self.book_expense(child_2, 3000) + + data = self.run_report() + self.assertEqual(self.row(data, child_1)["income"], 10000) + self.assertEqual(self.row(data, child_2)["expense"], 3000) + + parent_row = self.row(data, parent) + self.assertEqual(parent_row["income"], 10000) + self.assertEqual(parent_row["expense"], 3000) + self.assertEqual(parent_row["gross_profit_loss"], 7000) + + def test_date_range_excludes_out_of_period_entries(self): + cc = self.make_cc("_Test PA Date Range") + self.book_income(cc, 10000, posting_date="2025-06-01") + + # the 2025 income must not appear in a 2026 report (zero-value rows are dropped) + accounts_2026 = {r.get("account") for r in self.run_report()} + self.assertNotIn(cc, accounts_2026) + + row_2025 = self.row( + self.run_report( + fiscal_year="_Test Fiscal Year 2025", from_date="2025-01-01", to_date="2025-12-31" + ), + cc, + ) + self.assertEqual(row_2025["income"], 10000) + + def test_total_row_sums_income_and_expense(self): + cc = "_Test Cost Center - _TC" + self.book_income(cc, 10000) + self.book_expense(cc, 4000) + + data = self.run_report() + # the report appends a blank separator row and a totals row at the end + total_row = data[-1] + # the report wraps the (possibly translated) "Total" label in single quotes + self.assertEqual(total_row["account"], "'" + frappe._("Total") + "'") + # total is built from direct (non-accumulated) values, so it stays internally consistent + self.assertEqual(total_row["gross_profit_loss"], total_row["income"] - total_row["expense"]) + # and it includes this test's bookings + self.assertGreaterEqual(total_row["income"], 10000) + self.assertGreaterEqual(total_row["expense"], 4000) diff --git a/erpnext/accounts/report/purchase_invoice_trends/test_purchase_invoice_trends.py b/erpnext/accounts/report/purchase_invoice_trends/test_purchase_invoice_trends.py new file mode 100644 index 00000000000..f07555f3b13 --- /dev/null +++ b/erpnext/accounts/report/purchase_invoice_trends/test_purchase_invoice_trends.py @@ -0,0 +1,171 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.report.purchase_invoice_trends.purchase_invoice_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +FISCAL_YEAR = "_Test Fiscal Year 2026" +COMPANY = "_Test Company" +SUPPLIER = "_Test Supplier" +ITEM = "_Test Item" +POSTING_DATE = "2026-06-01" + + +def make_dated_purchase_invoice(qty, rate): + # make_purchase_invoice ignores posting_date unless posting time is explicitly set, so build the + # invoice unsubmitted, pin the posting date, then submit to land it in the intended period bucket. + pi = make_purchase_invoice( + supplier=SUPPLIER, item_code=ITEM, qty=qty, rate=rate, posting_date=POSTING_DATE, do_not_submit=1 + ) + pi.set_posting_time = 1 + pi.posting_date = POSTING_DATE + pi.submit() + return pi + + +class TestPurchaseInvoiceTrends(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": COMPANY, + "fiscal_year": FISCAL_YEAR, + "period": "Yearly", + "based_on": "Item", + } + ) + filters.update(extra) + columns, data = execute(filters) + labels = [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + return labels, data + + @staticmethod + def _cell(labels, row, label): + return row[labels.index(label)] + + def _find_row(self, data, key): + for row in data: + if row and row[0] == key: + return row + return None + + def test_yearly_item_qty_and_amount(self): + labels_before, data_before = self.run_report() + before = self._find_row(data_before, ITEM) + + qty, rate = 4, 250 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report() + self.assertIn("Item", labels) + self.assertIn("Item Name", labels) + self.assertIn("Currency", labels) + self.assertIn("Total(Qty)", labels) + self.assertIn("Total(Amt)", labels) + # Yearly period bucket uses the fiscal year name as the label prefix + self.assertIn(f"{FISCAL_YEAR} (Qty)", labels) + self.assertIn(f"{FISCAL_YEAR} (Amt)", labels) + + row = self._find_row(data, ITEM) + self.assertIsNotNone(row) + + before_qty = self._cell(labels_before, before, f"{FISCAL_YEAR} (Qty)") if before else 0 + before_amt = self._cell(labels_before, before, f"{FISCAL_YEAR} (Amt)") if before else 0 + before_tqty = self._cell(labels_before, before, "Total(Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, f"{FISCAL_YEAR} (Qty)") - before_qty, qty) + self.assertEqual(self._cell(labels, row, f"{FISCAL_YEAR} (Amt)") - before_amt, qty * rate) + self.assertEqual(self._cell(labels, row, "Total(Qty)") - before_tqty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) + + def test_monthly_bucket(self): + labels_before, data_before = self.run_report(period="Monthly") + before = self._find_row(data_before, ITEM) + + qty, rate = 3, 100 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(period="Monthly") + # posting_date 2026-06-01 -> June bucket + self.assertIn("Jun (Qty)", labels) + self.assertIn("Jun (Amt)", labels) + + row = self._find_row(data, ITEM) + before_qty = self._cell(labels_before, before, "Jun (Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Jun (Qty)") - before_qty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) + + def test_quarterly_bucket(self): + labels_before, data_before = self.run_report(period="Quarterly") + before = self._find_row(data_before, ITEM) + + qty, rate = 2, 150 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(period="Quarterly") + # 2026-06-01 falls in the Apr-Jun quarter + self.assertIn("Apr-Jun (Qty)", labels) + self.assertIn("Apr-Jun (Amt)", labels) + + row = self._find_row(data, ITEM) + before_qty = self._cell(labels_before, before, "Apr-Jun (Qty)") if before else 0 + before_amt = self._cell(labels_before, before, "Apr-Jun (Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Apr-Jun (Qty)") - before_qty, qty) + self.assertEqual(self._cell(labels, row, "Apr-Jun (Amt)") - before_amt, qty * rate) + + def test_based_on_supplier(self): + labels_before, data_before = self.run_report(based_on="Supplier") + before = self._find_row(data_before, SUPPLIER) + + qty, rate = 5, 200 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(based_on="Supplier") + self.assertIn("Supplier", labels) + self.assertIn("Supplier Name", labels) + self.assertIn("Supplier Group", labels) + + row = self._find_row(data, SUPPLIER) + self.assertIsNotNone(row) + + before_tqty = self._cell(labels_before, before, "Total(Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Total(Qty)") - before_tqty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) + + def test_group_by_item_under_supplier(self): + labels_before, data_before = self.run_report(based_on="Supplier", group_by="Item") + # group_by inserts an "Item" column; the item breakdown row carries the item key there + item_idx = labels_before.index("Item") + before = None + for r in data_before: + if r and r[0] != SUPPLIER and r[item_idx] == ITEM: + before = r + break + + qty, rate = 6, 300 + make_dated_purchase_invoice(qty, rate) + + labels, data = self.run_report(based_on="Supplier", group_by="Item") + self.assertIn("Item", labels) + + item_idx = labels.index("Item") + row = None + for r in data: + if r and r[0] != SUPPLIER and r[0] != "'Total'" and r[item_idx] == ITEM: + row = r + break + self.assertIsNotNone(row) + + before_tqty = self._cell(labels_before, before, "Total(Qty)") if before else 0 + before_tamt = self._cell(labels_before, before, "Total(Amt)") if before else 0 + + self.assertEqual(self._cell(labels, row, "Total(Qty)") - before_tqty, qty) + self.assertEqual(self._cell(labels, row, "Total(Amt)") - before_tamt, qty * rate) diff --git a/erpnext/accounts/report/received_items_to_be_billed/test_received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/test_received_items_to_be_billed.py new file mode 100644 index 00000000000..e24dff96f8e --- /dev/null +++ b/erpnext/accounts/report/received_items_to_be_billed/test_received_items_to_be_billed.py @@ -0,0 +1,101 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.received_items_to_be_billed.received_items_to_be_billed import execute +from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice as make_pi_from_pr +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.tests.utils import ERPNextTestSuite + + +class TestReceivedItemsToBeBilled(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "posting_date": "2026-06-30", + } + ) + filters.update(extra) + return execute(filters)[1] + + def get_row(self, data, purchase_receipt): + matches = [row for row in data if row.get("name") == purchase_receipt] + return matches[0] if matches else None + + def test_unbilled_receipt_appears_with_pending_amount(self): + pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + + row = self.get_row(self.run_report(), pr.name) + + self.assertIsNotNone(row, "Unbilled Purchase Receipt should appear in the report") + self.assertEqual(row.get("supplier"), "_Test Supplier") + self.assertEqual(row.get("item_code"), "_Test Item") + self.assertEqual(row.get("amount"), 1000.0) + self.assertEqual(row.get("billed_amount"), 0.0) + self.assertEqual(row.get("returned_amount"), 0.0) + self.assertEqual(row.get("pending_amount"), 1000.0) + + def test_billed_receipt_drops_out_of_report(self): + pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + + self.assertIsNotNone(self.get_row(self.run_report(), pr.name)) + + pi = make_pi_from_pr(pr.name) + pi.set_posting_time = 1 + pi.posting_date = "2026-06-02" + pi.submit() + + self.assertIsNone( + self.get_row(self.run_report(), pr.name), + "Fully billed Purchase Receipt should no longer appear in the report", + ) + + def test_reference_field_filter_limits_to_single_receipt(self): + first_pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + second_pr = make_purchase_receipt( + item_code="_Test Item", + qty=3, + rate=100, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + + data = self.run_report(purchase_receipt=first_pr.name) + + self.assertIsNotNone(self.get_row(data, first_pr.name)) + self.assertIsNone(self.get_row(data, second_pr.name)) + + def test_posting_date_cutoff_excludes_later_receipts(self): + pr = make_purchase_receipt( + item_code="_Test Item", + qty=5, + rate=200, + supplier="_Test Supplier", + posting_date="2026-06-15", + ) + + self.assertIsNone( + self.get_row(self.run_report(posting_date="2026-06-01"), pr.name), + "Receipt dated after the cutoff should be excluded", + ) + self.assertIsNotNone(self.get_row(self.run_report(posting_date="2026-06-30"), pr.name)) diff --git a/erpnext/accounts/report/sales_invoice_trends/test_sales_invoice_trends.py b/erpnext/accounts/report/sales_invoice_trends/test_sales_invoice_trends.py new file mode 100644 index 00000000000..98979b1cbda --- /dev/null +++ b/erpnext/accounts/report/sales_invoice_trends/test_sales_invoice_trends.py @@ -0,0 +1,118 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.sales_invoice_trends.sales_invoice_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +FISCAL_YEAR = "_Test Fiscal Year 2026" +POSTING_DATE = "2026-06-01" + + +class TestSalesInvoiceTrends(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": FISCAL_YEAR, + "based_on": "Item", + "period": "Yearly", + } + ) + filters.update(extra) + columns, data = execute(filters) + labels = [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + return labels, data + + def _cell(self, data, key_label, key_value, col_label, labels): + """Return the value at column `col_label` for the row whose first-column + value equals `key_value`, or 0 if that row does not exist yet.""" + key_idx = labels.index(key_label) + col_idx = labels.index(col_label) + for row in data: + if row[key_idx] == key_value: + return row[col_idx] or 0 + return 0 + + def test_yearly_item_amount_and_total(self): + # Yearly period => a single " (Qty)"/"(Amt)" bucket, plus Total(Qty)/Total(Amt). + labels, before = self.run_report() + qty_col = f"{FISCAL_YEAR} (Qty)" + amt_col = f"{FISCAL_YEAR} (Amt)" + before_qty = self._cell(before, "Item", "_Test Item", qty_col, labels) + before_amt = self._cell(before, "Item", "_Test Item", amt_col, labels) + before_tot_qty = self._cell(before, "Item", "_Test Item", "Total(Qty)", labels) + before_tot_amt = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + create_sales_invoice(item="_Test Item", qty=4, rate=200, posting_date=POSTING_DATE) + + labels, after = self.run_report() + self.assertEqual(self._cell(after, "Item", "_Test Item", qty_col, labels) - before_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", amt_col, labels) - before_amt, 800) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Qty)", labels) - before_tot_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_tot_amt, 800) + + def test_monthly_lands_in_june_bucket(self): + # Monthly period => one bucket per month; a 2026-06-01 invoice hits "Jun (Qty)"/"(Amt)". + labels, before = self.run_report(period="Monthly") + before_qty = self._cell(before, "Item", "_Test Item", "Jun (Qty)", labels) + before_amt = self._cell(before, "Item", "_Test Item", "Jun (Amt)", labels) + before_tot = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + create_sales_invoice(item="_Test Item", qty=3, rate=100, posting_date=POSTING_DATE) + + labels, after = self.run_report(period="Monthly") + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Qty)", labels) - before_qty, 3) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Amt)", labels) - before_amt, 300) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_tot, 300) + # Nothing should leak into an unrelated month. + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jan (Amt)", labels), 0) + + def test_quarterly_lands_in_apr_jun_bucket(self): + # Quarterly period over a Jan-Dec fiscal year => Apr-Jun is the 2nd quarter; June lands there. + labels, before = self.run_report(period="Quarterly") + before_qty = self._cell(before, "Item", "_Test Item", "Apr-Jun (Qty)", labels) + before_amt = self._cell(before, "Item", "_Test Item", "Apr-Jun (Amt)", labels) + + create_sales_invoice(item="_Test Item", qty=5, rate=50, posting_date=POSTING_DATE) + + labels, after = self.run_report(period="Quarterly") + self.assertEqual(self._cell(after, "Item", "_Test Item", "Apr-Jun (Qty)", labels) - before_qty, 5) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Apr-Jun (Amt)", labels) - before_amt, 250) + # Jan-Mar quarter must stay untouched. + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jan-Mar (Amt)", labels), 0) + + def test_based_on_customer_total(self): + # based_on=Customer => first column is "Customer"; the customer's Total(Amt) reflects the sale. + labels, before = self.run_report(based_on="Customer") + before_tot_qty = self._cell(before, "Customer", "_Test Customer", "Total(Qty)", labels) + before_tot_amt = self._cell(before, "Customer", "_Test Customer", "Total(Amt)", labels) + + create_sales_invoice( + customer="_Test Customer", item="_Test Item", qty=2, rate=300, posting_date=POSTING_DATE + ) + + labels, after = self.run_report(based_on="Customer") + self.assertEqual( + self._cell(after, "Customer", "_Test Customer", "Total(Qty)", labels) - before_tot_qty, 2 + ) + self.assertEqual( + self._cell(after, "Customer", "_Test Customer", "Total(Amt)", labels) - before_tot_amt, 600 + ) + + def test_group_by_item_under_customer(self): + # based_on=Customer + group_by=Item inserts an "Item" breakdown column before the period + # buckets; the per-item detail row carries the item key and the amount for that customer/item. + labels, before = self.run_report(based_on="Customer", group_by="Item") + # In group_by mode the detail rows key off the group_by column ("Item"), so snapshot by item. + before_amt = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + create_sales_invoice( + customer="_Test Customer", item="_Test Item", qty=6, rate=100, posting_date=POSTING_DATE + ) + + labels, after = self.run_report(based_on="Customer", group_by="Item") + self.assertIn("Item", labels) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_amt, 600) diff --git a/erpnext/accounts/report/share_balance/share_balance.py b/erpnext/accounts/report/share_balance/share_balance.py index 1d02a996b76..b58c439a14c 100644 --- a/erpnext/accounts/report/share_balance/share_balance.py +++ b/erpnext/accounts/report/share_balance/share_balance.py @@ -15,8 +15,6 @@ def execute(filters=None): columns = get_columns(filters) - filters.get("date") - data = [] if not filters.get("shareholder"): @@ -24,7 +22,7 @@ def execute(filters=None): else: share_type, no_of_shares, rate, amount = 1, 2, 3, 4 - all_shares = get_all_shares(filters.get("shareholder")) + all_shares = get_all_shares(filters.get("shareholder"), filters.get("date"), filters.get("company")) for share_entry in all_shares: row = False for datum in data: @@ -63,5 +61,47 @@ def get_columns(filters): return columns -def get_all_shares(shareholder): - return frappe.get_doc("Shareholder", shareholder).share_balance +def get_all_shares(shareholder, date, company=None): + """Share movements for the shareholder up to (and including) `date`, signed by direction: + shares received are positive, shares transferred/sold out are negative. + + The shareholder and company predicates are pushed into the query so only the + relevant transfers are fetched instead of scanning the whole table.""" + share_transfer = frappe.qb.DocType("Share Transfer") + query = ( + frappe.qb.from_(share_transfer) + .select( + share_transfer.share_type, + share_transfer.no_of_shares, + share_transfer.rate, + share_transfer.amount, + share_transfer.from_shareholder, + share_transfer.to_shareholder, + ) + .where((share_transfer.docstatus == 1) & (share_transfer.date <= date)) + .where( + (share_transfer.to_shareholder == shareholder) | (share_transfer.from_shareholder == shareholder) + ) + .orderby(share_transfer.date) + ) + + if company: + query = query.where(share_transfer.company == company) + + transfers = query.run(as_dict=True) + + shares = [] + for transfer in transfers: + if transfer.to_shareholder == shareholder: + shares.append(transfer) + elif transfer.from_shareholder == shareholder: + shares.append( + frappe._dict( + share_type=transfer.share_type, + no_of_shares=-transfer.no_of_shares, + rate=transfer.rate, + amount=-transfer.amount, + ) + ) + + return shares diff --git a/erpnext/accounts/report/share_balance/test_share_balance.py b/erpnext/accounts/report/share_balance/test_share_balance.py new file mode 100644 index 00000000000..0b91d1525f3 --- /dev/null +++ b/erpnext/accounts/report/share_balance/test_share_balance.py @@ -0,0 +1,201 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.share_balance.share_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" + + +class TestShareBalanceReport(ERPNextTestSuite): + def setUp(self): + self.share_type = create_share_type("_Test Share Balance Equity") + self.shareholder = create_shareholder("_Test Share Balance Holder", COMPANY) + + def test_date_filter_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({"shareholder": self.shareholder})) + + def test_no_shareholder_returns_empty_data(self): + # `shareholder` is optional; without it the report yields no rows. + columns, data = execute(frappe._dict({"date": "2026-06-01", "company": COMPANY})) + self.assertEqual(data, []) + self.assertEqual(len(columns), 5) + + def test_balance_after_issue(self): + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + + row = self.get_row(date="2026-06-05") + self.assertEqual(row[0], self.shareholder) + self.assertEqual(row[1], self.share_type) + self.assertEqual(row[2], 100) # no_of_shares + self.assertEqual(row[3], 10) # average rate + self.assertEqual(row[4], 1000) # amount = 100 * 10 + + def test_company_filter_scopes_transfers(self): + # the transfer is booked under `_Test Company` + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + + # matching company: the holding shows up + self.assertEqual(self.get_row(date="2026-06-05")[2], 100) + + # a different company must not surface this shareholder's transfer + other_company_data = execute( + frappe._dict( + {"date": "2026-06-05", "company": "_Test Company 1", "shareholder": self.shareholder} + ) + )[1] + self.assertEqual(other_company_data, []) + + def test_balance_increases_on_second_issue(self): + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=101, + to_no=200, + no_of_shares=100, + rate=20, + date="2026-06-10", + ) + + # The report groups by share type, summing shares and amount and + # recomputing the average rate: (1000 + 2000) / 200 = 15. + row = self.get_row(date="2026-06-15") + self.assertEqual(row[2], 200) + self.assertEqual(row[3], 15) + self.assertEqual(row[4], 3000) + + def test_balance_reduces_after_transfer_out(self): + other_holder = create_shareholder("_Test Share Balance Holder 2", COMPANY) + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + create_share_transfer( + transfer_type="Transfer", + from_shareholder=self.shareholder, + to_shareholder=other_holder, + share_type=self.share_type, + from_no=1, + to_no=40, + no_of_shares=40, + rate=10, + date="2026-06-10", + ) + + row = self.get_row(date="2026-06-15") + self.assertEqual(row[2], 60) # 100 issued - 40 transferred out + self.assertEqual(row[4], 600) + + other_row = self.get_row(date="2026-06-15", shareholder=other_holder) + self.assertEqual(other_row[2], 40) + self.assertEqual(other_row[4], 400) + + def test_as_on_date_before_issue_shows_no_holding(self): + # the report is as-on `date`: before any share transfer, the shareholder holds nothing + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + + data = execute( + frappe._dict({"date": "2026-05-01", "company": COMPANY, "shareholder": self.shareholder}) + )[1] + self.assertEqual(data, []) + + def test_as_on_date_reflects_holding_up_to_that_date(self): + # two issues on different dates; an as-on date between them sees only the first + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=1, + to_no=100, + no_of_shares=100, + rate=10, + date="2026-06-01", + ) + create_share_transfer( + transfer_type="Issue", + to_shareholder=self.shareholder, + share_type=self.share_type, + from_no=101, + to_no=200, + no_of_shares=100, + rate=20, + date="2026-06-10", + ) + + self.assertEqual(self.get_row(date="2026-06-05")[2], 100) # only the first issue + self.assertEqual(self.get_row(date="2026-06-15")[2], 200) # both issues + + def get_row(self, date, shareholder=None): + filters = frappe._dict( + {"date": date, "company": COMPANY, "shareholder": shareholder or self.shareholder} + ) + data = execute(filters)[1] + holdings = [r for r in data if r[1] == self.share_type] + self.assertEqual(len(holdings), 1, f"Expected one row for share type, got: {data}") + return holdings[0] + + +def create_share_type(title): + if not frappe.db.exists("Share Type", title): + frappe.get_doc({"doctype": "Share Type", "title": title}).insert() + return title + + +def create_shareholder(title, company): + shareholder = frappe.get_doc({"doctype": "Shareholder", "title": title, "company": company}).insert() + return shareholder.name + + +def create_share_transfer(**kwargs): + kwargs.setdefault("company", COMPANY) + kwargs.setdefault("asset_account", "Cash - _TC") + kwargs.setdefault("equity_or_liability_account", "Creditors - _TC") + transfer = frappe.get_doc({"doctype": "Share Transfer", **kwargs}) + transfer.submit() + return transfer diff --git a/erpnext/accounts/report/share_ledger/test_share_ledger.py b/erpnext/accounts/report/share_ledger/test_share_ledger.py new file mode 100644 index 00000000000..51309bd9f94 --- /dev/null +++ b/erpnext/accounts/report/share_ledger/test_share_ledger.py @@ -0,0 +1,171 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.report.share_ledger.share_ledger import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" + +# The report returns legacy positional columns (no fieldnames); name the indices once +# here so a column reorder needs a single edit instead of silently shifting assertions. +COL_SHAREHOLDER = 0 +COL_DATE = 1 +COL_TRANSFER_TYPE = 2 +COL_SHARE_TYPE = 3 +COL_NO_OF_SHARES = 4 +COL_RATE = 5 +COL_AMOUNT = 6 +COL_COMPANY = 7 +COL_SHARE_TRANSFER = 8 + + +class TestShareLedger(ERPNextTestSuite): + def setUp(self): + self.shareholder = self.create_shareholder("_Test Share Ledger Holder") + # Issue 100 shares on 2026-06-01, then another 50 on 2026-06-10. + self.first = self.issue_shares(date="2026-06-01", from_no=1, to_no=100, rate=10) + self.second = self.issue_shares(date="2026-06-10", from_no=101, to_no=150, rate=12) + + def test_ledger_lists_all_transfers_upto_date(self): + data = self.run_report(shareholder=self.shareholder, date="2026-06-30") + + self.assertEqual(len(data), 2) + + first_row, second_row = data + self.assertEqual(first_row[COL_SHAREHOLDER], self.shareholder) + self.assertEqual(first_row[COL_DATE], frappe.utils.getdate("2026-06-01")) + self.assertEqual(first_row[COL_TRANSFER_TYPE], "Issue") + self.assertEqual(first_row[COL_SHARE_TYPE], "Equity") + self.assertEqual(first_row[COL_NO_OF_SHARES], 100) + self.assertEqual(first_row[COL_RATE], 10) + self.assertEqual(first_row[COL_AMOUNT], 1000) + self.assertEqual(first_row[COL_COMPANY], COMPANY) + self.assertEqual(first_row[COL_SHARE_TRANSFER], self.first) + + self.assertEqual(second_row[COL_DATE], frappe.utils.getdate("2026-06-10")) + self.assertEqual(second_row[COL_NO_OF_SHARES], 50) + self.assertEqual(second_row[COL_RATE], 12) + self.assertEqual(second_row[COL_AMOUNT], 600) + self.assertEqual(second_row[COL_SHARE_TRANSFER], self.second) + + def test_running_balance_of_shares(self): + data = self.run_report(shareholder=self.shareholder, date="2026-06-30") + + # The ledger records each transfer's raw no_of_shares (always positive); it does + # not sign by direction. With only incoming "Issue" rows here, summing them is a + # valid running total. (Directional balances are the Share Balance report's job.) + running = 0 + balances = [] + for row in data: + running += row[COL_NO_OF_SHARES] + balances.append(running) + + self.assertEqual(balances, [100, 150]) + + def test_as_on_date_between_transfers_shows_only_first(self): + data = self.run_report(shareholder=self.shareholder, date="2026-06-05") + + self.assertEqual(len(data), 1) + self.assertEqual(data[0][COL_SHARE_TRANSFER], self.first) + self.assertEqual(data[0][COL_NO_OF_SHARES], 100) + + def test_transfer_type_label_when_shareholder_is_seller(self): + buyer = self.create_shareholder("_Test Share Ledger Buyer") + transfer = self.make_transfer( + from_shareholder=self.shareholder, + to_shareholder=buyer, + date="2026-06-15", + from_no=1, + to_no=40, + rate=10, + ) + + row = self.transfer_row(self.run_report(shareholder=self.shareholder, date="2026-06-30"), transfer) + # seller side: the label names the counterparty it went "to" + self.assertEqual(row[COL_TRANSFER_TYPE], f"Transfer to {buyer}") + + def test_transfer_type_label_when_shareholder_is_buyer(self): + seller = self.create_shareholder("_Test Share Ledger Seller") + # the seller must own shares before it can transfer them + self.issue_shares(date="2026-06-12", from_no=201, to_no=300, rate=10, shareholder=seller) + transfer = self.make_transfer( + from_shareholder=seller, + to_shareholder=self.shareholder, + date="2026-06-15", + from_no=201, + to_no=240, + rate=10, + ) + + row = self.transfer_row(self.run_report(shareholder=self.shareholder, date="2026-06-30"), transfer) + # buyer side: the label names the counterparty it came "from" + self.assertEqual(row[COL_TRANSFER_TYPE], f"Transfer from {seller}") + + def test_missing_date_throws(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict(shareholder=self.shareholder)) + + def test_missing_shareholder_returns_no_rows(self): + data = self.run_report(date="2026-06-30") + self.assertEqual(data, []) + + def run_report(self, **extra): + filters = frappe._dict({"company": COMPANY, **extra}) + return execute(filters)[1] + + def transfer_row(self, data, transfer_name): + row = next((r for r in data if r[COL_SHARE_TRANSFER] == transfer_name), None) + self.assertIsNotNone(row, f"Share Transfer {transfer_name} missing from ledger") + return row + + def create_shareholder(self, title): + doc = frappe.get_doc( + { + "doctype": "Shareholder", + "title": title, + "company": COMPANY, + } + ).insert() + return doc.name + + def issue_shares(self, date, from_no, to_no, rate, shareholder=None): + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Issue", + "date": date, + "to_shareholder": shareholder or self.shareholder, + "share_type": "Equity", + "from_no": from_no, + "to_no": to_no, + "no_of_shares": to_no - from_no + 1, + "rate": rate, + "company": COMPANY, + "asset_account": "Cash - _TC", + "equity_or_liability_account": "Creditors - _TC", + } + ) + doc.submit() + return doc.name + + def make_transfer(self, from_shareholder, to_shareholder, date, from_no, to_no, rate): + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": date, + "from_shareholder": from_shareholder, + "to_shareholder": to_shareholder, + "share_type": "Equity", + "from_no": from_no, + "to_no": to_no, + "no_of_shares": to_no - from_no + 1, + "rate": rate, + "company": COMPANY, + "asset_account": "Cash - _TC", + "equity_or_liability_account": "Creditors - _TC", + } + ) + doc.submit() + return doc.name diff --git a/erpnext/accounts/report/trial_balance/trial_balance.json b/erpnext/accounts/report/trial_balance/trial_balance.json index 6793268a1e6..5a8bd5c006e 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.json +++ b/erpnext/accounts/report/trial_balance/trial_balance.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-22 13:38:42.740436", + "modified": "2026-07-01 17:32:21.801141", "modified_by": "Administrator", "module": "Accounts", "name": "Trial Balance", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 0, + "snapshot_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d8fce97263e..15862f5746e 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -583,7 +583,7 @@ def hide_group_accounts(data): return non_group_accounts_data -def execute_synced_report(filters): +def execute_snapshot_report(filters): from frappe.database.duckdb.database import get_latest_sync if conn := get_latest_sync("GL Entry"): diff --git a/erpnext/accounts/report/voucher_wise_balance/test_voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/test_voucher_wise_balance.py new file mode 100644 index 00000000000..be04141e4b6 --- /dev/null +++ b/erpnext/accounts/report/voucher_wise_balance/test_voucher_wise_balance.py @@ -0,0 +1,63 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.report.voucher_wise_balance.voucher_wise_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestVoucherWiseBalance(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + } + ) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, voucher_no): + for row in data: + if row.get("voucher_no") == voucher_no: + return row + return None + + def test_balanced_voucher_not_flagged(self): + jv = make_journal_entry( + "Sales - _TC", "_Test Bank - _TC", 1000, submit=True, posting_date="2026-06-01" + ) + + data = self.run_report() + self.assertIsNone( + self.find_row(data, jv.name), + msg="A balanced voucher (debit == credit) must not be flagged.", + ) + + def test_imbalanced_voucher_flagged(self): + jv = make_journal_entry( + "Sales - _TC", "_Test Bank - _TC", 1000, submit=True, posting_date="2026-06-01" + ) + + # Tamper one GL Entry: drop the debit side so debit != credit for this voucher. + gle_name = frappe.db.get_value( + "GL Entry", + {"voucher_no": jv.name, "is_cancelled": 0, "debit": [">", 0]}, + "name", + ) + self.assertIsNotNone(gle_name, msg="Expected a debit GL Entry for the journal entry.") + frappe.db.set_value("GL Entry", gle_name, {"debit": 400, "debit_in_account_currency": 400}) + + data = self.run_report() + row = self.find_row(data, jv.name) + self.assertIsNotNone(row, msg="An imbalanced voucher must be flagged by the report.") + + self.assertEqual(row.get("voucher_type"), "Journal Entry") + self.assertEqual(row.get("credit"), 1000) + self.assertEqual(row.get("debit"), 400) + self.assertNotEqual( + row.get("debit"), row.get("credit"), msg="Flagged rows must have debit != credit." + ) diff --git a/erpnext/accounts/workspace/accounts_setup/accounts_setup.json b/erpnext/accounts/workspace/accounts_setup/accounts_setup.json new file mode 100644 index 00000000000..88dd071b131 --- /dev/null +++ b/erpnext/accounts/workspace/accounts_setup/accounts_setup.json @@ -0,0 +1,329 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-14 12:44:31.994274", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "database", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Accounts Setup", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 13:43:50.138704", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Accounts Setup", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 55.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 0, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Chart of Accounts", + "link_to": "Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Chart of Cost Centers", + "link_to": "Cost Center", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Account Category", + "link_to": "Account Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounting Dimension", + "link_to": "Accounting Dimension", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency", + "link_to": "Currency", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency Exchange", + "link_to": "Currency Exchange", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Finance Book", + "link_to": "Finance Book", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Mode of Payment", + "link_to": "Mode of Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Term", + "link_to": "Payment Term", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Journal Entry Template", + "link_to": "Journal Entry Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Terms and Conditions", + "link_to": "Terms and Conditions", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Company", + "link_to": "Company", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Fiscal Year", + "link_to": "Fiscal Year", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Taxes", + "link_to": "Sales Taxes and Charges Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "lock-keyhole-open", + "indent": 1, + "keep_closed": 0, + "label": "Opening & Closing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "COA Importer", + "link_to": "Chart of Accounts Importer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Opening Invoice Tool", + "link_to": "Opening Invoice Creation Tool", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Accounting Period", + "link_to": "Accounting Period", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "FX Revaluation", + "link_to": "Exchange Rate Revaluation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Period Closing Voucher", + "link_to": "Period Closing Voucher", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 1, + "keep_closed": 0, + "label": "Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Accounts Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency Exchange Settings", + "link_to": "Currency Exchange Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Accounts Setup", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/banking/banking.json b/erpnext/accounts/workspace/banking/banking.json new file mode 100644 index 00000000000..072af4a7193 --- /dev/null +++ b/erpnext/accounts/workspace/banking/banking.json @@ -0,0 +1,222 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:22.767176", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "circle-dollar-sign", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Banking", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 13:43:50.924019", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Banking", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 49.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "book-open-check", + "indent": 0, + "keep_closed": 0, + "label": "Bank Clearance", + "link_to": "Bank Clearance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "tool", + "indent": 0, + "keep_closed": 0, + "label": "Bank Reconciliation", + "link_to": "Bank Reconciliation Tool", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "clipboard-check", + "indent": 0, + "keep_closed": 0, + "label": "Reconciliation Statement", + "link_to": "Bank Reconciliation Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "split", + "indent": 0, + "keep_closed": 0, + "label": "Unreconcile Payment", + "link_to": "Unreconcile Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "link", + "indent": 0, + "keep_closed": 0, + "label": "Process Payment Reconciliation", + "link_to": "Process Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Bank", + "link_to": "Bank", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Bank Account", + "link_to": "Bank Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bank Account Type", + "link_to": "Bank Account Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bank Account Subtype", + "link_to": "Bank Account Subtype", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bank Guarantee", + "link_to": "Bank Guarantee", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Plaid Settings", + "link_to": "Plaid Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "scroll-text", + "indent": 1, + "keep_closed": 1, + "label": "Dunning", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Dunning", + "link_to": "Dunning", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Dunning Type", + "link_to": "Dunning Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Banking", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/budgeting/budgeting.json b/erpnext/accounts/workspace/budgeting/budgeting.json new file mode 100644 index 00000000000..03e1d96b6e8 --- /dev/null +++ b/erpnext/accounts/workspace/budgeting/budgeting.json @@ -0,0 +1,104 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-14 14:38:20.315394", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "accounting", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Budgeting", + "link_type": "DocType", + "links": [], + "modified": "2026-07-02 04:24:48.116724", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Budgeting", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 57.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "briefcase-business", + "indent": 0, + "keep_closed": 0, + "label": "Budget", + "link_to": "Budget", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "badge-cent", + "indent": 0, + "keep_closed": 0, + "label": "Cost Center", + "link_to": "Cost Center", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "accounting", + "indent": 0, + "keep_closed": 0, + "label": "Accounting Dimension", + "link_to": "Accounting Dimension", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "notepad-text", + "indent": 0, + "keep_closed": 0, + "label": "Cost Center Allocation", + "link_to": "Cost Center Allocation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "sheet", + "indent": 0, + "keep_closed": 0, + "label": "Budget Variance", + "link_to": "Budget Variance Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Budgeting", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/financial_reports/financial_reports.json b/erpnext/accounts/workspace/financial_reports/financial_reports.json index 5ce20bed642..3ad09d26e52 100644 --- a/erpnext/accounts/workspace/financial_reports/financial_reports.json +++ b/erpnext/accounts/workspace/financial_reports/financial_reports.json @@ -13,7 +13,7 @@ "doctype": "Workspace", "for_user": "", "hide_custom": 0, - "icon": "table", + "icon": "sheet", "idx": 1, "indicator_color": "", "is_hidden": 0, @@ -266,9 +266,10 @@ "type": "Link" } ], - "modified": "2026-05-18 09:49:45.138296", + "modified": "2026-06-14 13:44:08.095321", "modified_by": "Administrator", "module": "Accounts", + "module_onboarding": "Accounting Onboarding", "name": "Financial Reports", "number_cards": [], "owner": "Administrator", @@ -279,6 +280,417 @@ "roles": [], "sequence_id": 5.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "accounting", + "indent": 1, + "keep_closed": 0, + "label": "Financial Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Balance Sheet", + "link_to": "Balance Sheet", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Profit and Loss", + "link_to": "Profit and Loss Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Cash Flow", + "link_to": "Cash Flow", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance", + "link_to": "Trial Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Consolidated Report", + "link_to": "Consolidated Financial Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Custom Financial Statement", + "link_to": "Custom Financial Statement", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Financial Report Template", + "link_to": "Financial Report Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "book-text", + "indent": 1, + "keep_closed": 0, + "label": "Ledgers", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "General Ledger", + "link_to": "General Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Ledger", + "link_to": "Customer Ledger Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Ledger", + "link_to": "Supplier Ledger Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "indent": 1, + "keep_closed": 1, + "label": "Registers", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Receivable", + "link_to": "Accounts Receivable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Payable", + "link_to": "Accounts Payable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "AR Summary", + "link_to": "Accounts Receivable Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "AP Summary", + "link_to": "Accounts Payable Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Register", + "link_to": "Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Register", + "link_to": "Purchase Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise sales Register", + "link_to": "Item-wise Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Purchase Register", + "link_to": "Item-wise Purchase Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "dollar-sign", + "indent": 1, + "keep_closed": 1, + "label": "Profitability", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Gross Profit", + "link_to": "Gross Profit", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Profitability Analysis", + "link_to": "Profitability Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice Trends", + "link_to": "Sales Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice Trends", + "link_to": "Purchase Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "scroll-text", + "indent": 1, + "keep_closed": 1, + "label": "Other Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance for Party", + "link_to": "Trial Balance for Party", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Period Based On Invoice Date", + "link_to": "Payment Period Based On Invoice Date", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partners Commission", + "link_to": "Sales Partners Commission", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Credit Balance", + "link_to": "Customer Credit Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Payment Summary", + "link_to": "Sales Payment Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Address And Contacts", + "link_to": "Address And Contacts", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "UAE VAT 201", + "link_to": "UAE VAT 201", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Financial Reports", "type": "Workspace" } diff --git a/erpnext/accounts/workspace/invoicing/invoicing.json b/erpnext/accounts/workspace/invoicing/invoicing.json index 30ffe9a8f16..f34ea417b25 100644 --- a/erpnext/accounts/workspace/invoicing/invoicing.json +++ b/erpnext/accounts/workspace/invoicing/invoicing.json @@ -587,9 +587,10 @@ "type": "Link" } ], - "modified": "2026-01-23 11:05:47.246213", + "modified": "2026-06-14 13:44:08.471142", "modified_by": "Administrator", "module": "Accounts", + "module_onboarding": "Accounting Onboarding", "name": "Invoicing", "number_cards": [ { @@ -617,6 +618,354 @@ "roles": [], "sequence_id": 2.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Invoicing", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Accounts", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "list-tree", + "indent": 0, + "keep_closed": 0, + "label": "Chart of Accounts", + "link_to": "Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-left-to-line", + "indent": 1, + "keep_closed": 0, + "label": "Receivables", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Credit Note", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "route_options": "{\"is_return\": 1}", + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Receivable", + "link_to": "Accounts Receivable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-right-from-line", + "indent": 1, + "keep_closed": 0, + "label": "Payables", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice", + "link_to": "Purchase Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Debit Note", + "link_to": "Purchase Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "route_options": "{\"is_return\": 1}", + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Payable", + "link_to": "Accounts Payable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "money-coins-1", + "indent": 1, + "keep_closed": 0, + "label": "Payments", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Entry", + "link_to": "Payment Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Journal Entry", + "link_to": "Journal Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Request", + "link_to": "Payment Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Order", + "link_to": "Payment Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Unreconcile Payment", + "link_to": "Unreconcile Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Process Payment Reconciliation", + "link_to": "Process Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Accounting Ledger", + "link_to": "Repost Accounting Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Payment Ledger", + "link_to": "Repost Payment Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 0, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "General Ledger", + "link_to": "General Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance", + "link_to": "Trial Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Financial Reports", + "link_to": "Financial Reports", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Invoicing", "type": "Workspace" } diff --git a/erpnext/accounts/workspace/payments/payments.json b/erpnext/accounts/workspace/payments/payments.json new file mode 100644 index 00000000000..118e2961298 --- /dev/null +++ b/erpnext/accounts/workspace/payments/payments.json @@ -0,0 +1,240 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:21.886461", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "receipt-text", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Payments", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 13:43:50.184761", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Payments", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 47.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Payments", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "money-coins-1", + "indent": 1, + "keep_closed": 0, + "label": "Payments", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Entry", + "link_to": "Payment Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Journal Entry", + "link_to": "Journal Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Request", + "link_to": "Payment Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Order", + "link_to": "Payment Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Payment Reconciliation", + "link_to": "Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Unreconcile Payment", + "link_to": "Unreconcile Payment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Process Payment Reconciliation", + "link_to": "Process Payment Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Accounting Ledger", + "link_to": "Repost Accounting Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Repost Payment Ledger", + "link_to": "Repost Payment Ledger", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Receivable", + "link_to": "Accounts Receivable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Accounts Payable", + "link_to": "Accounts Payable", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "General Ledger", + "link_to": "General Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Trial Balance", + "link_to": "Trial Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Financial Reports", + "link_to": "Financial Reports", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Payments", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/share_management/share_management.json b/erpnext/accounts/workspace/share_management/share_management.json new file mode 100644 index 00000000000..6766b4ea9a4 --- /dev/null +++ b/erpnext/accounts/workspace/share_management/share_management.json @@ -0,0 +1,86 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:22.831729", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "money-coins-1", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Share Management", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 13:43:51.040978", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Share Management", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 50.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 1, + "collapsible": 1, + "icon": "customer", + "indent": 0, + "keep_closed": 0, + "label": "Shareholder", + "link_to": "Shareholder", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "move-horizontal", + "indent": 0, + "keep_closed": 0, + "label": "Share Transfer", + "link_to": "Share Transfer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "list", + "indent": 0, + "keep_closed": 0, + "label": "Share Ledger", + "link_to": "Share Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "notepad-text", + "indent": 0, + "keep_closed": 0, + "label": "Share Balance", + "link_to": "Share Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Share Management", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/subscriptions/subscriptions.json b/erpnext/accounts/workspace/subscriptions/subscriptions.json new file mode 100644 index 00000000000..750573eb38c --- /dev/null +++ b/erpnext/accounts/workspace/subscriptions/subscriptions.json @@ -0,0 +1,121 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-14 14:08:36.817393", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "accounting", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Subscriptions", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 14:08:36.999272", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Subscriptions", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 56.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "circle-dollar-sign", + "indent": 0, + "keep_closed": 0, + "label": "Subscription", + "link_to": "Subscription", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Subscription Plan", + "link_to": "Subscription Plan", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Subscription Settings", + "link_to": "Subscription Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Subscriptions", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/taxes/taxes.json b/erpnext/accounts/workspace/taxes/taxes.json new file mode 100644 index 00000000000..f2ccf3aa7e7 --- /dev/null +++ b/erpnext/accounts/workspace/taxes/taxes.json @@ -0,0 +1,188 @@ +{ + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:22.649582", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "money-coins-1", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Taxes", + "link_type": "DocType", + "links": [], + "modified": "2026-06-14 13:43:50.894825", + "modified_by": "Administrator", + "module": "Accounts", + "module_onboarding": "Accounting Onboarding", + "name": "Taxes", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 48.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "panel-bottom-close", + "indent": 0, + "keep_closed": 0, + "label": "Sales Tax Template", + "link_to": "Sales Taxes and Charges Template", + "link_type": "DocType", + "navigate_to_tab": "", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "panel-top-close", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Tax Template", + "link_to": "Purchase Taxes and Charges Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Item Tax Template", + "link_to": "Item Tax Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "triangle", + "indent": 0, + "keep_closed": 0, + "label": "Tax Category", + "link_to": "Tax Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "book-open-text", + "indent": 0, + "keep_closed": 0, + "label": "Tax Rule", + "link_to": "Tax Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "book-text", + "indent": 0, + "keep_closed": 0, + "label": "Tax Withholding Category", + "link_to": "Tax Withholding Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Tax Withholding Group", + "link_to": "Tax Withholding Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notebook-text", + "indent": 0, + "keep_closed": 0, + "label": "Deduction Certificate", + "link_to": "Lower Deduction Certificate", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_to": "", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "TDS Computation Summary", + "link_to": "TDS Computation Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Tax Withholding Details", + "link_to": "Tax Withholding Details", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Taxes", + "type": "Workspace" +} diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index 82c22dac559..fae323faad2 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -199,9 +199,10 @@ "type": "Link" } ], - "modified": "2025-12-31 16:22:38.132729", + "modified": "2026-06-14 13:44:08.417956", "modified_by": "Administrator", "module": "Assets", + "module_onboarding": "Asset Onboarding", "name": "Assets", "number_cards": [], "owner": "Administrator", @@ -212,6 +213,294 @@ "roles": [], "sequence_id": 7.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Assets", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Asset", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "laptop", + "indent": 0, + "keep_closed": 0, + "label": "Asset", + "link_to": "Asset", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "trending-down", + "indent": 0, + "keep_closed": 0, + "label": "Depreciation Schedule", + "link_to": "Asset Depreciation Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sprout", + "indent": 0, + "keep_closed": 0, + "label": "Asset Capitalization", + "link_to": "Asset Capitalization", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "move-horizontal", + "indent": 0, + "keep_closed": 0, + "label": "Asset Movement", + "link_to": "Asset Movement", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Maintenance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance Team", + "link_to": "Asset Maintenance Team", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance", + "link_to": "Asset Maintenance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance Log", + "link_to": "Asset Maintenance Log", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Value Adjustment", + "link_to": "Asset Value Adjustment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Repair", + "link_to": "Asset Repair", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Fixed Asset Register", + "link_to": "Fixed Asset Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Depreciation Ledger", + "link_to": "Asset Depreciation Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Depreciations and Balances", + "link_to": "Asset Depreciations and Balances", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Maintenance", + "link_to": "Asset Maintenance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Activity", + "link_to": "Asset Activity", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Asset Category", + "link_to": "Asset Category", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Location", + "link_to": "Location", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "navigate_to_tab": "assets_tab", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link", + "url": "" + } + ], + "standard": 1, "title": "Assets", "type": "Workspace" } diff --git a/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py new file mode 100644 index 00000000000..7399d30d2b9 --- /dev/null +++ b/erpnext/buying/report/item_wise_purchase_history/test_item_wise_purchase_history.py @@ -0,0 +1,130 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice +from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_pr_against_po, + create_purchase_order, +) +from erpnext.buying.report.item_wise_purchase_history.item_wise_purchase_history import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestItemWisePurchaseHistory(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + **extra, + } + ) + return execute(filters) + + def po_row(self, po_name, **extra): + data = self.run_report(**extra)[1] + return next(row for row in data if row["purchase_order"] == po_name) + + def test_purchase_order_line_shown_with_values(self): + po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01") + + row = self.po_row(po.name) + self.assertEqual(row["item_code"], "_Test Item") + self.assertEqual(row["quantity"], 10) + self.assertEqual(row["rate"], 500) + self.assertEqual(row["amount"], 5000) + self.assertEqual(row["supplier"], "_Test Supplier") + + def test_draft_purchase_order_excluded(self): + po = create_purchase_order(transaction_date="2026-06-01", do_not_submit=True) + + names = {row["purchase_order"] for row in self.run_report()[1]} + self.assertNotIn(po.name, names) + + def test_date_range_filters_on_transaction_date(self): + po = create_purchase_order(transaction_date="2026-06-01") + + in_range = { + row["purchase_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1] + } + self.assertIn(po.name, in_range) + + out_of_range = { + row["purchase_order"] for row in self.run_report(from_date="2026-01-01", to_date="2026-03-01")[1] + } + self.assertNotIn(po.name, out_of_range) + + def test_item_code_filter(self): + po = create_purchase_order( + transaction_date="2026-06-01", + rm_items=[ + {"item_code": "_Test Item", "qty": 5, "rate": 500, "warehouse": "_Test Warehouse - _TC"}, + {"item_code": "_Test Item 2", "qty": 3, "rate": 200, "warehouse": "_Test Warehouse - _TC"}, + ], + ) + + rows = self.run_report(item_code="_Test Item 2")[1] + self.assertEqual({row["item_code"] for row in rows}, {"_Test Item 2"}) + # the filtered-out line of the same order must not leak in + self.assertTrue(all(row["purchase_order"] == po.name for row in rows)) + + def test_item_group_filter(self): + # _Test Item is in _Test Item Group; _Test FG Item is in _Test Item Group Desktops + po_test_group = create_purchase_order(item_code="_Test Item", transaction_date="2026-06-01") + po_other_group = create_purchase_order(item_code="_Test FG Item", transaction_date="2026-06-01") + + names = {row["purchase_order"] for row in self.run_report(item_group="_Test Item Group")[1]} + self.assertIn(po_test_group.name, names) + self.assertNotIn(po_other_group.name, names) + + def test_supplier_filter(self): + create_purchase_order(supplier="_Test Supplier", transaction_date="2026-06-01") + create_purchase_order(supplier="_Test Supplier 1", transaction_date="2026-06-01") + + suppliers = {row["supplier"] for row in self.run_report(supplier="_Test Supplier")[1]} + self.assertEqual(suppliers, {"_Test Supplier"}) + + def test_received_quantity_reflects_receipt(self): + po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01") + create_pr_against_po(po.name, received_qty=4) + + self.assertEqual(self.po_row(po.name)["received_qty"], 4) + + def test_billed_amount_reflects_invoice(self): + po = create_purchase_order(qty=10, rate=500, transaction_date="2026-06-01") + pi = make_purchase_invoice(po.name) + pi.insert() + pi.submit() + + self.assertEqual(self.po_row(po.name)["billed_amt"], 5000) + + def test_amounts_reported_in_company_currency(self): + # a USD order must report rate/amount converted to the company's currency (base_* fields) + po = create_purchase_order( + do_not_save=True, + currency="USD", + qty=10, + rate=100, + transaction_date="2026-06-01", + ) + po.conversion_rate = 80 + po.insert() + po.submit() + + row = self.po_row(po.name) + self.assertEqual(row["rate"], 8000) # 100 USD * 80 + self.assertEqual(row["amount"], 80000) # 10 * 100 USD * 80 + + def test_chart_aggregates_amount_per_item(self): + create_purchase_order(item_code="_Test Item", qty=2, rate=500, transaction_date="2026-06-01") + create_purchase_order(item_code="_Test Item", qty=3, rate=500, transaction_date="2026-06-01") + + chart = self.run_report(item_code="_Test Item")[3] + labels = chart["data"]["labels"] + values = chart["data"]["datasets"][0]["values"] + self.assertIn("_Test Item", labels) + # 2*500 + 3*500 aggregated for the item + self.assertEqual(values[labels.index("_Test Item")], 2500) diff --git a/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py b/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py new file mode 100644 index 00000000000..35cd9ebac58 --- /dev/null +++ b/erpnext/buying/report/purchase_analytics/test_purchase_analytics.py @@ -0,0 +1,93 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.buying.report.purchase_analytics.purchase_analytics import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" +SUPPLIER = "_Test Supplier" +SUPPLIER_GROUP = "_Test Supplier Group" +# A historical window that ordinary test fixtures don't post into. +FROM_DATE = "2019-04-01" +TO_DATE = "2019-06-30" + + +class TestPurchaseAnalytics(ERPNextTestSuite): + """purchase_analytics reuses the shared Analytics engine; these tests lock its + wiring (doc_type=Purchase Order) across the Supplier Group / Item Group trees.""" + + def setUp(self): + frappe.set_user("Administrator") + + def _filters(self, **overrides): + filters = { + "doc_type": "Purchase Order", + "value_quantity": "Value", + "range": "Monthly", + "company": COMPANY, + "from_date": FROM_DATE, + "to_date": TO_DATE, + } + filters.update(overrides) + return frappe._dict(filters) + + def _rows(self, filters): + return {row["entity"]: row for row in execute(filters)[1]} + + def make_po(self, qty=4, rate=250): + return create_purchase_order( + company=COMPANY, supplier=SUPPLIER, qty=qty, rate=rate, transaction_date="2019-04-10" + ) + + def test_supplier_group_tree_rolls_up_to_root(self): + filters = self._filters(tree_type="Supplier Group") + base = self._rows(filters) + base_group = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0)) + + po = self.make_po(qty=4, rate=250) + rows = self._rows(filters) + + # supplier is remapped to its group; the root sits at indent 0 + self.assertIn(SUPPLIER_GROUP, rows) + self.assertIn("All Supplier Groups", rows) + self.assertNotIn(SUPPLIER, rows) + self.assertEqual(rows["All Supplier Groups"]["indent"], 0) + + self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_group, flt(po.base_net_total), places=2) + self.assertGreaterEqual(flt(rows["All Supplier Groups"]["total"]), flt(po.base_net_total)) + + def test_item_group_tree_rolls_up_to_root(self): + item_group = frappe.db.get_value("Item", "_Test Item", "item_group") + filters = self._filters(tree_type="Item Group") + base = self._rows(filters) + base_group = flt(base.get(item_group, {}).get("total", 0.0)) + + po = self.make_po(qty=4, rate=250) + rows = self._rows(filters) + + self.assertIn(item_group, rows) + self.assertIn("All Item Groups", rows) + # the raw item code must not leak as its own entity; the root sits at indent 0 + self.assertNotIn("_Test Item", rows) + self.assertEqual(rows["All Item Groups"]["indent"], 0) + self.assertAlmostEqual(rows[item_group]["total"] - base_group, flt(po.base_net_total), places=2) + self.assertGreaterEqual(flt(rows["All Item Groups"]["total"]), flt(po.base_net_total)) + + def test_supplier_group_by_quantity(self): + filters = self._filters(tree_type="Supplier Group", value_quantity="Quantity") + base = self._rows(filters) + base_qty = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0)) + base_root_qty = flt(base.get("All Supplier Groups", {}).get("total", 0.0)) + + po = self.make_po(qty=7, rate=100) + rows = self._rows(filters) + + self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_qty, flt(po.total_qty), places=2) + # the quantity must roll up to the root too, not just the leaf group + self.assertAlmostEqual( + rows["All Supplier Groups"]["total"] - base_root_qty, flt(po.total_qty), places=2 + ) diff --git a/erpnext/buying/report/subcontract_order_summary/test_subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/test_subcontract_order_summary.py new file mode 100644 index 00000000000..e6e0922eaf2 --- /dev/null +++ b/erpnext/buying/report/subcontract_order_summary/test_subcontract_order_summary.py @@ -0,0 +1,49 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, today + +from erpnext.buying.report.subcontract_order_summary.subcontract_order_summary import execute +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_subcontracted_items, +) +from erpnext.tests.utils import ERPNextTestSuite + +FG_ITEM = "Subcontracted Item SA7" + + +class TestSubcontractOrderSummary(ERPNextTestSuite): + """The report lists Subcontracting Order finished items with their ordered and + received quantities within the transaction-date window.""" + + def setUp(self): + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + def run_report(self, **extra): + filters = frappe._dict( + {"company": "_Test Company", "from_date": add_days(today(), -1), "to_date": add_days(today(), 1)} + ) + filters.update(extra) + return execute(filters)[1] + + def test_subcontracting_order_is_listed(self): + sco = get_subcontracting_order() + + rows = [r for r in self.run_report(name=sco.name) if r.get("item_code") == FG_ITEM] + self.assertTrue(rows, "Subcontracting Order finished item missing from report") + self.assertEqual(rows[0]["qty"], 10) + self.assertEqual(rows[0]["received_qty"], 0) # nothing received yet + + def test_out_of_range_date_excludes_order(self): + sco = get_subcontracting_order() + + data = self.run_report(name=sco.name, from_date="2019-01-01", to_date="2019-01-31") + self.assertEqual(data, []) diff --git a/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py new file mode 100644 index 00000000000..d32a7cabfcc --- /dev/null +++ b/erpnext/buying/report/supplier_quotation_comparison/test_supplier_quotation_comparison.py @@ -0,0 +1,66 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company" +ITEM = "_Test Item" + + +class TestSupplierQuotationComparison(ERPNextTestSuite): + """The report lists Supplier Quotation item lines so quotes for the same item can + be compared across suppliers.""" + + def make_quotation(self, supplier, qty, rate, uom=None): + item = {"item_code": ITEM, "qty": qty, "rate": rate, "warehouse": "_Test Warehouse - _TC"} + if uom: + item["uom"] = uom + sq = frappe.get_doc( + { + "doctype": "Supplier Quotation", + "supplier": supplier, + "company": COMPANY, + "currency": "INR", + "transaction_date": "2026-06-01", + "items": [item], + } + ) + sq.insert() + sq.submit() + return sq + + def run_report(self, **extra): + filters = frappe._dict({"company": COMPANY, "from_date": "2026-01-01", "to_date": "2026-12-31"}) + filters.update(extra) + return execute(filters)[1] + + def test_no_filters_returns_empty(self): + self.assertEqual(execute(None)[1], []) + + def test_quotation_line_listed_with_price(self): + # _Test UOM 1 converts at 10 stock units per qty, so price_per_unit + # (amount / stock_qty) diverges from base_rate and the division path is tested + sq = self.make_quotation("_Test Supplier", qty=10, rate=100, uom="_Test UOM 1") + + rows = [r for r in self.run_report(item_code=ITEM) if r.get("quotation") == sq.name] + self.assertTrue(rows, "Supplier Quotation line missing from report") + row = rows[0] + self.assertEqual(row["supplier_name"], "_Test Supplier") + self.assertEqual(row["qty"], 10) + self.assertEqual(row["base_rate"], 100) + self.assertEqual(row["base_amount"], 1000) + # 1000 amount / (10 qty * 10 conversion) = 10, distinct from the 100 base_rate + self.assertEqual(row["price_per_unit"], 10) + + def test_compares_multiple_suppliers_for_item(self): + sq1 = self.make_quotation("_Test Supplier", qty=10, rate=100) + sq2 = self.make_quotation("_Test Supplier 1", qty=10, rate=120) + + quotes = {r["quotation"]: r for r in self.run_report(item_code=ITEM)} + self.assertIn(sq1.name, quotes) + self.assertIn(sq2.name, quotes) + self.assertEqual(quotes[sq1.name]["base_rate"], 100) + self.assertEqual(quotes[sq2.name]["base_rate"], 120) diff --git a/erpnext/buying/workspace/buying/buying.json b/erpnext/buying/workspace/buying/buying.json index 6bc20901467..268501949a7 100644 --- a/erpnext/buying/workspace/buying/buying.json +++ b/erpnext/buying/workspace/buying/buying.json @@ -341,17 +341,6 @@ "onboard": 1, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 1, - "label": "Item Wise Consumption", - "link_count": 0, - "link_to": "Item Wise Consumption", - "link_type": "Report", - "onboard": 1, - "type": "Link" - }, { "dependencies": "", "hidden": 0, @@ -512,9 +501,10 @@ "type": "Link" } ], - "modified": "2026-01-02 14:55:59.078773", + "modified": "2026-06-14 13:43:50.509039", "modified_by": "Administrator", "module": "Buying", + "module_onboarding": "Buying Onboarding", "name": "Buying", "number_cards": [ { @@ -538,6 +528,403 @@ "roles": [], "sequence_id": 5.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Buying", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Buying", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 0, + "keep_closed": 0, + "label": "Material Request", + "link_to": "Material Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "git-pull-request-arrow", + "indent": 0, + "keep_closed": 0, + "label": "Request for Quotation", + "link_to": "Request for Quotation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "book-open-text", + "indent": 0, + "keep_closed": 0, + "label": "Supplier Quotation", + "link_to": "Supplier Quotation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order", + "link_to": "Purchase Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "liabilities", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice", + "link_to": "Purchase Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Group", + "link_to": "Supplier Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Price List", + "link_to": "Price List", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Address", + "link_to": "Address", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contacts", + "link_to": "Contact", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard", + "link_to": "Supplier Scorecard", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard Criteria", + "link_to": "Supplier Scorecard Criteria", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard Variable", + "link_to": "Supplier Scorecard Variable", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Scorecard Standing", + "link_to": "Supplier Scorecard Standing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Analytics", + "link_to": "Purchase Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order Analysis", + "link_to": "Purchase Order Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Requested Items to Order and Receive", + "link_to": "Requested Items to Order and Receive", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Items To Be Requested", + "link_to": "Items To Be Requested", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Purchase History", + "link_to": "Item-wise Purchase History", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Receipt Trends ", + "link_to": "Purchase Receipt Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Invoice Trends", + "link_to": "Purchase Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order Trends", + "link_to": "Purchase Order Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Procurement Tracker", + "link_to": "Procurement Tracker", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Wise Consumption", + "link_to": "Item Wise Consumption", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Quotation Comparison", + "link_to": "Supplier Quotation Comparison", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Supplier Addresses And Contacts", + "link_to": "Address And Contacts", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Buying Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Buying", "type": "Workspace" } diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 9e46598768f..eed56008547 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -167,7 +167,8 @@ status_map = { "Pick List": [ ["Draft", None], ["Open", "eval:self.docstatus == 1"], - ["Completed", "stock_entry_exists"], + ["Completed", "is_fully_transferred"], + ["Partially Transferred", "is_partially_transferred"], [ "Partly Delivered", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'", diff --git a/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py b/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py new file mode 100644 index 00000000000..745fc85aec6 --- /dev/null +++ b/erpnext/crm/report/lead_owner_efficiency/test_lead_owner_efficiency.py @@ -0,0 +1,71 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, today + +from erpnext.crm.report.lead_owner_efficiency.lead_owner_efficiency import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestLeadOwnerEfficiency(ERPNextTestSuite): + """Groups leads by their owner and counts the opportunity/quotation/order funnel + derived from those leads.""" + + def setUp(self): + # a unique owner keeps the per-owner counts isolated from other tests' leads + self.owner = self.make_user() + + def make_user(self): + email = f"lead_owner_{frappe.generate_hash(length=8)}@example.com" + frappe.get_doc( + {"doctype": "User", "email": email, "first_name": "Lead Owner", "send_welcome_email": 0} + ).insert() + return email + + def make_lead(self): + return frappe.get_doc( + { + "doctype": "Lead", + "lead_name": f"Lead {frappe.generate_hash(length=6)}", + "lead_owner": self.owner, + "company": "_Test Company", + } + ).insert() + + def run_report(self, **extra): + filters = frappe._dict({"from_date": add_days(today(), -1), "to_date": today()}) + filters.update(extra) + return execute(filters)[1] + + def owner_row(self, data): + return next((r for r in data if r["lead_owner"] == self.owner), None) + + def test_lead_count_grouped_by_owner(self): + self.make_lead() + self.make_lead() + + row = self.owner_row(self.run_report()) + self.assertIsNotNone(row, "Lead owner missing from report") + self.assertEqual(row["lead_count"], 2) + self.assertEqual(row["opp_count"], 0) + self.assertEqual(row["opp_lead"], 0.0) + + def test_opportunity_from_lead_is_counted(self): + lead = self.make_lead() + frappe.get_doc( + { + "doctype": "Opportunity", + "opportunity_from": "Lead", + "party_name": lead.name, + "company": "_Test Company", + "currency": "INR", + } + ).insert() + + row = self.owner_row(self.run_report()) + self.assertIsNotNone(row, "Lead owner missing from report") + self.assertEqual(row["lead_count"], 1) + self.assertEqual(row["opp_count"], 1) + # one opportunity from one lead -> 100% opp/lead conversion + self.assertEqual(row["opp_lead"], 100.0) diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index 59814c87e4a..52e1a1acbfb 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -2,11 +2,11 @@ "app": "erpnext", "charts": [ { - "chart_name": "Won Opportunities", - "label": "Won Opportunities" + "chart_name": "Territory Wise Sales", + "label": "Territory Wise Sales" } ], - "content": "[{\"id\":\"4jhDsfZ7EP\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe CRM instead.\",\"col\":12}},{\"id\":\"-bzBQ_IbL9\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Won Opportunities\",\"col\":12}},{\"id\":\"LdM1QgUnqU\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"New Lead (Last 1 Month)\",\"col\":4}},{\"id\":\"X23-SXBcYG\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"New Opportunity (Last 1 Month)\",\"col\":4}},{\"id\":\"3rm7fH52M-\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Won Opportunity (Last 1 Month)\",\"col\":4}},{\"id\":\"K6a2Kh5Zav\",\"type\":\"spacer\",\"data\":{\"col\":12}}]", + "content": "[{\"id\":\"4jhDsfZ7EP\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe CRM instead.\",\"col\":12}},{\"id\":\"Cj2TyhgiWy\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"id\":\"LAKRmpYMRA\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"69RN0XsiJK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead\",\"col\":3}},{\"id\":\"t6PQ0vY-Iw\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Opportunity\",\"col\":3}},{\"id\":\"VOFE0hqXRD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"0ik53fuemG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"id\":\"wdROEmB_XG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"-I9HhcgUKE\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"ttpROKW9vk\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"-76QPdbBHy\",\"type\":\"card\",\"data\":{\"card_name\":\"Sales Pipeline\",\"col\":4}},{\"id\":\"_YmGwzVWRr\",\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"id\":\"Bma1PxoXk3\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"80viA0R83a\",\"type\":\"card\",\"data\":{\"card_name\":\"Campaign\",\"col\":4}},{\"id\":\"Buo5HtKRFN\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"sLS_x4FMK2\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}}]", "creation": "2020-01-23 14:48:30.183272", "custom_blocks": [], "docstatus": 0, @@ -18,6 +18,14 @@ "is_hidden": 0, "label": "CRM", "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "Lead", "hidden": 0, @@ -115,6 +123,14 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Maintenance", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -148,6 +164,183 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Sales Pipeline", + "link_count": 7, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Lead", + "link_count": 0, + "link_to": "Lead", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Opportunity", + "link_count": 0, + "link_to": "Opportunity", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customer", + "link_count": 0, + "link_to": "Customer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Contract", + "link_count": 0, + "link_to": "Contract", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Appointment", + "link_count": 0, + "link_to": "Appointment", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_count": 0, + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Communication", + "link_count": 0, + "link_to": "Communication", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 2, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "CRM Settings", + "link_count": 0, + "link_to": "CRM Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Settings", + "link_count": 0, + "link_to": "SMS Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Campaign", + "link_count": 5, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Campaign", + "link_count": 0, + "link_to": "Campaign", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Campaign", + "link_count": 0, + "link_to": "Email Campaign", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Center", + "link_count": 0, + "link_to": "SMS Center", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Log", + "link_count": 0, + "link_to": "SMS Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Group", + "link_count": 0, + "link_to": "Email Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -228,24 +421,11 @@ "type": "Link" } ], - "modified": "2026-01-03 15:05:23.983099", + "modified": "2026-06-14 13:44:08.297053", "modified_by": "Administrator", "module": "CRM", "name": "CRM", - "number_cards": [ - { - "label": "New Lead (Last 1 Month)", - "number_card_name": "New Lead (Last 1 Month)" - }, - { - "label": "New Opportunity (Last 1 Month)", - "number_card_name": "New Opportunity (Last 1 Month)" - }, - { - "label": "Won Opportunity (Last 1 Month)", - "number_card_name": "Won Opportunity (Last 1 Month)" - } - ], + "number_cards": [], "owner": "Administrator", "parent_page": "", "public": 1, @@ -253,7 +433,552 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 17.0, - "shortcuts": [], + "shortcuts": [ + { + "color": "Blue", + "format": "{} Open", + "label": "Lead", + "link_to": "Lead", + "stats_filter": "{\"status\":\"Open\"}", + "type": "DocType" + }, + { + "color": "Blue", + "format": "{} Assigned", + "label": "Opportunity", + "link_to": "Opportunity", + "stats_filter": "{\"_assign\": [\"like\", '%' + frappe.session.user + '%']}", + "type": "DocType" + }, + { + "label": "Customer", + "link_to": "Customer", + "type": "DocType" + }, + { + "label": "Sales Analytics", + "link_to": "Sales Analytics", + "report_ref_doctype": "Sales Order", + "type": "Report" + }, + { + "label": "Dashboard", + "link_to": "CRM", + "type": "Dashboard" + } + ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "CRM", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "users-round", + "indent": 0, + "keep_closed": 0, + "label": "Lead", + "link_to": "Lead", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "lightbulb", + "indent": 0, + "keep_closed": 0, + "label": "Opportunity", + "link_to": "Opportunity", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "customer", + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Sales Analytics", + "link_to": "Sales Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead Details", + "link_to": "Lead Details", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Pipeline Analytics", + "link_to": "Sales Pipeline Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Opportunity Summary by Sales Stage", + "link_to": "Opportunity Summary by Sales Stage", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Funnel", + "link_to": "sales-funnel", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Prospects Engaged But Not Converted", + "link_to": "Prospects Engaged But Not Converted", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "First Response Time for Opportunity", + "link_to": "First Response Time for Opportunity", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Campaign Efficiency", + "link_to": "Campaign Efficiency", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead Owner Efficiency", + "link_to": "Lead Owner Efficiency", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Maintenance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Schedule", + "link_to": "Maintenance Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Visit", + "link_to": "Maintenance Visit", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Warranty Claim", + "link_to": "Warranty Claim", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "funnel", + "indent": 1, + "keep_closed": 1, + "label": "Sales Pipeline", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead", + "link_to": "Lead", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Opportunity", + "link_to": "Opportunity", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contract", + "link_to": "Contract", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Appointment", + "link_to": "Appointment", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Communication", + "link_to": "Communication", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sell", + "indent": 1, + "keep_closed": 1, + "label": "Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Campaign", + "link_to": "Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Email Campaign", + "link_to": "Email Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "SMS Center", + "link_to": "SMS Center", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "SMS Log", + "link_to": "SMS Log", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Email Group", + "link_to": "Email Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Territory", + "link_to": "Territory", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Group", + "link_to": "Customer Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contact", + "link_to": "Contact", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Prospect", + "link_to": "Prospect", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person", + "link_to": "Sales Person", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Stage", + "link_to": "Sales Stage", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Lead Source", + "link_to": "UTM Source", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 1, + "keep_closed": 1, + "label": "Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "CRM Settings", + "link_to": "CRM Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "SMS Settings", + "link_to": "SMS Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "CRM", "type": "Workspace" } diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 115e17fd789..cc808075fe4 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -8,7 +8,7 @@ app_email = "hello@frappe.io" app_license = "GNU General Public License (v3)" source_link = "https://github.com/frappe/erpnext" app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" -app_home = "/desk" +app_home = "/desk/home" add_to_apps_screen = [ { diff --git a/erpnext/locale/bs.po b/erpnext/locale/bs.po index cf5e37e713c..681a4eddbc6 100644 --- a/erpnext/locale/bs.po +++ b/erpnext/locale/bs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-28 20:03\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Bosnian\n" "MIME-Version: 1.0\n" @@ -269,7 +269,7 @@ msgstr "'Dozvoli višestruke Prodajne Naloge naspram Nabavnog Naloga Klijenta'" #: erpnext/controllers/trends.py:62 msgid "'Based On' and 'Group By' can not be the same" -msgstr "" +msgstr "'Na Osnovu' i 'Grupiraj Po' ne mogu biti isti" #: erpnext/selling/report/inactive_customers/inactive_customers.py:23 msgid "'Days Since Last Order' must be greater than or equal to zero" @@ -295,15 +295,15 @@ msgstr "'Od datuma' mora biti nakon 'Do datuma'" #: erpnext/stock/doctype/item/item.py:466 msgid "'Has Serial No' cannot be 'Yes' for non-stock item" -msgstr "" +msgstr "'Ima Serijski Broj' ne može biti 'Da' za artikal koji nije na zalihama" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:145 msgid "'Inspection Required before Delivery' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Dostave' je onemogućena za artikal {0}, nema potrebe za kreiranjem Kontrole Kvaliteta" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:136 msgid "'Inspection Required before Purchase' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Nabave' je onemogućena za artikal {0}, nema potrebe za kreiranjem Kontrole Kvaliteta" #: erpnext/stock/report/stock_ledger/stock_ledger.py:684 #: erpnext/stock/report/stock_ledger/stock_ledger.py:725 @@ -323,7 +323,7 @@ msgstr "'Do Paketa Broj' ne može biti manje od 'Od Paketa Broj.'" #: erpnext/controllers/sales_and_purchase_return.py:80 msgid "'Update Stock' cannot be checked because items are not delivered via {0}" -msgstr "" +msgstr "'Ažuriraj Zalihe' ne se može provjeriti jer artikli nisu dostavljeni putem {0}" #: erpnext/accounts/doctype/sales_invoice/services/fixed_assets.py:46 msgid "'Update Stock' cannot be checked for fixed asset sale" @@ -827,7 +827,7 @@ msgstr "

Ne može se fakturisati više od predviđenog iznosa za sljedeće art #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:159 msgid "

Following {0}s do not belong to Company {1}:

" -msgstr "" +msgstr "

Slijedeći {0} ne pripadaju {1}:

" #. Content of the 'html_llwp' (HTML) field in DocType 'Request for Quotation' #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -1042,7 +1042,7 @@ msgstr "A - C" #: erpnext/selling/doctype/customer/customer.py:358 msgid "A Customer Group exists with the same name. Please change the Customer name or rename the Customer Group" -msgstr "" +msgstr "Grupa Klijenta postoji sa istim imenom, preimenujte klijenta ili preimenujte Grupu Klijenta" #: erpnext/manufacturing/doctype/workstation/workstation.js:73 msgid "A Holiday List can be added to exclude counting these days for the Workstation." @@ -1054,7 +1054,7 @@ msgstr "Potencijalni Klijent zahtijeva ili ime osobe ili ime poduzeća" #: erpnext/stock/doctype/packing_slip/packing_slip.py:83 msgid "A Packing Slip can only be created for a Draft Delivery Note." -msgstr "" +msgstr "Nalog Pakovanja se može kreirati samo za nacrt Dostavnice." #: erpnext/accounts/services/gl_validator.py:123 msgid "A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more." @@ -1312,7 +1312,7 @@ msgstr "Pristupni ključ je potreban za davaoca usluga: {0}" #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:423 msgid "Access to Request for Quotation from the portal is disabled. To allow access, enable it in Portal Settings." -msgstr "" +msgstr "Pristup Zahtjevu za Ponudu sa portala je onemogućen. Da biste omogućili pristup, omogući ga u Postavkama Portala." #. Description of the 'Common Code' (Data) field in DocType 'UOM' #: erpnext/setup/doctype/uom/uom.json @@ -2062,7 +2062,7 @@ msgstr "Knjigovodstveni Period" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:49 msgid "Accounting Period cannot be created for a future date. End Date {0} is after today." -msgstr "" +msgstr "Knjigovodstveni Period se ne može kreirati za budući datum. Datum završetka {0} je sutra." #: erpnext/accounts/doctype/accounting_period/accounting_period.py:77 msgid "Accounting Period overlaps with {0}" @@ -2921,7 +2921,7 @@ msgstr "Dodata uloga dobavljača korisniku {0}." #: erpnext/controllers/website_list_for_contact.py:311 msgid "Added {1} role to user {0}." -msgstr "" +msgstr "Dodana je uloga {1} korisniku {0}." #: erpnext/crm/doctype/lead/lead.js:81 msgid "Adding Lead to Prospect..." @@ -3183,7 +3183,7 @@ msgstr "Dodatna Prenesena Količina" #: erpnext/manufacturing/doctype/work_order/work_order.py:591 msgid "Additional Transferred Qty {0} cannot be greater than {1}. To fix this, increase the percentage value of the field 'Transfer Extra Raw Materials to WIP' in Manufacturing Settings." -msgstr "" +msgstr "Dodatna Prenesena Količina {0} ne može biti veća od {1}. Da biste ovo ispravili, povećajte procentualnu vrijednost 'Prenesi Dodatne Sirovine u Nedovršenu Proizvodnju' u Postavkama Proizvodnje." #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:630 msgid "Additional {0} {1} of item {2} required as per BOM to complete this transaction" @@ -3779,7 +3779,7 @@ msgstr "Algoritam" #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/selling/doctype/customer/customer.json msgid "Alias" -msgstr "" +msgstr "Nadimak" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:165 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:185 @@ -3989,7 +3989,7 @@ msgstr "Svi komentari i e-pošta kopirat će se iz jednog dokumenta u drugi novo #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:204 msgid "All the items have already been returned." -msgstr "" +msgstr "Svi artikli su već vraćeni." #: erpnext/manufacturing/doctype/work_order/work_order.js:1272 msgid "All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table." @@ -3997,7 +3997,7 @@ msgstr "Svi obavezni Artikli (sirovine) bit će preuzeti iz Sastavnice i popunje #: erpnext/stock/doctype/delivery_note/mapper.py:82 msgid "All these items have already been invoiced/returned" -msgstr "" +msgstr "Svi ovi artikli su već fakturisani/vraćeni" #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:100 #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:101 @@ -4144,7 +4144,7 @@ msgstr "Dozvoli Alternativni Artikal" #: erpnext/stock/doctype/item_alternative/item_alternative.py:68 msgid "Allow Alternative Item must be checked on Item {0}" -msgstr "" +msgstr "Dozvoli Alternativni Artikal mora biti odabrano za Artikal {0}" #. Label of the material_consumption (Check) field in DocType 'Manufacturing #. Settings' @@ -4536,7 +4536,7 @@ msgstr "Dozvoljena Transakcija sa" #. Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Allowed Users" -msgstr "" +msgstr "Dozvoljeni Korisnici" #: erpnext/accounts/doctype/party_link/party_link.py:27 msgid "Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only." @@ -5459,7 +5459,7 @@ msgstr "Termin s" #: erpnext/www/book_appointment/index.js:237 msgid "Appointment created successfully" -msgstr "" +msgstr "Termin je uspješno zakazan" #: erpnext/crm/doctype/appointment/appointment.py:101 msgid "Appointment was created. But no lead was found. Please check the email to confirm" @@ -5505,11 +5505,11 @@ msgstr "Jeste li sigurni da želite izbrisati sve demo podatke?" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:51 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:100 msgid "Are you sure you want to create Reposting Entries?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:66 msgid "Are you sure you want to create a Reposting Entry?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:480 msgid "Are you sure you want to delete this Item?" @@ -5603,7 +5603,7 @@ msgstr "Pošto ima dovoljno sirovina, Materijalni Nalog nije potreban za Skladi #: erpnext/stock/doctype/stock_settings/stock_settings.py:250 msgid "As there is reserved stock, you cannot disable {0}." -msgstr "" +msgstr "Pošto postoje rezervisane zalihe, ne možete onemogućiti {0}." #: erpnext/stock/doctype/stock_settings/stock_settings.py:224 #: erpnext/stock/doctype/stock_settings/stock_settings.py:236 @@ -6194,7 +6194,7 @@ msgstr "Dodijeli Imenu" #: erpnext/buying/doctype/purchase_order/purchase_order.js:593 #: erpnext/public/js/controllers/buying.js:555 msgid "Assigning {0} to {1} (row {2})" -msgstr "" +msgstr "Dodjeljuje se {0} {1} (red {2})" #. Label of the filters_section (Section Break) field in DocType 'Service Level #. Agreement' @@ -6220,7 +6220,7 @@ msgstr "U Redu {0}: U Serijskom i Šaržnom Paketu {1} mora imati status dokumen #: erpnext/accounts/services/internal_transfer.py:98 msgid "At Row {0}: The field {1} is mandatory for internal transfer" -msgstr "" +msgstr "U Redu {0}: Polje {1} je obavezno za interni prenos" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:85 msgid "At least one account with exchange gain or loss is required" @@ -6253,7 +6253,7 @@ msgstr "Najmanje jedno od Prodaje ili Nabave mora biti odabrano" #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:226 msgid "At least one raw material for Finished Good Item {0} should be customer provided." -msgstr "" +msgstr "Klijent treba da obezbijedi barem jednu sirovinu za gotov proizvod {0}." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:60 msgid "At least one raw material item must be present in the stock entry for the type {0}" @@ -6293,7 +6293,7 @@ msgstr "Red {0}: Serijski Broj je obavezan za Artikal {1}" #: erpnext/stock/services/serial_batch_bundle_service.py:498 msgid "At row {0}: Serial and Batch Bundle {1} has already been created. Please remove the values from the serial no or batch no fields." -msgstr "" +msgstr "U Redu {0}: Serijski i Šaržni Paket {1} je već stvoren. Uklonite vrijednosti iz polja za serijski ili šaržni broj." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:123 msgid "At row {0}: set Parent Row No for item {1}" @@ -6930,7 +6930,7 @@ msgstr "Sastavnica 1" #: erpnext/manufacturing/doctype/bom/mapper.py:82 msgid "BOM 1 {0} and BOM 2 {1} should not be the same" -msgstr "" +msgstr "Sastavnica 1 {0} i Sastavnica 2 {1} ne bi trebale biti iste" #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:38 msgid "BOM 2" @@ -7188,7 +7188,7 @@ msgstr "Rekurzija Sastavnice: {1} ne može biti nadređena ili podređena {0}" #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js:81 msgid "BOM update is queued and may take a few minutes. Check {0} for progress." -msgstr "" +msgstr "Ažuriranje Sastavnice je u redu čekanja i može potrajati nekoliko minuta. Provjeri {0} za napredak." #: erpnext/manufacturing/doctype/bom/bom.py:1404 msgid "BOM {0} does not belong to Item {1}" @@ -7351,7 +7351,7 @@ msgstr "Sažetak Bilansa Stanja" #: erpnext/accounts/report/balance_sheet/balance_sheet.py:284 msgid "Balance Sheet requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilansa Stanja zahtijeva da se {0} sinhronizira s DuckDB-om" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html:13 msgid "Balance Stock Qty" @@ -7514,7 +7514,7 @@ msgstr "Tip Bankovnog Računa" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:443 msgid "Bank Account {0} in Bank Transaction {1} is not matching with Bank Account {2}" -msgstr "" +msgstr "Bankovni Račun {0} u Bankovnoj Transakciji {1} nije usklađen s Bankovnim Računom {2}" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:15 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:20 @@ -8096,7 +8096,7 @@ msgstr "Broj Šarže je obavezan" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3495 msgid "Batch No {0} does not exist" -msgstr "" +msgstr "Broj Šarže {0} ne postoji" #: erpnext/stock/utils.py:625 msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." @@ -8108,7 +8108,7 @@ msgstr "Broj Šarže {0} nije prisutan u originalnom {1} {2}, stoga ga ne možet #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:658 msgid "Batch No {0} of Item {1} has negative stock of quantity {2} in the warehouse {3}" -msgstr "" +msgstr "Broj Šarže {0} Artikla {1} ima negativnu količinu zaliha {2} u skladištu {3}" #. Label of the batch_no (Int) field in DocType 'BOM Update Batch' #: erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -8177,7 +8177,7 @@ msgstr "Šarža i Serijski Broj" #: erpnext/manufacturing/doctype/work_order/work_order.py:742 msgid "Batch not created for item {0} since it does not have a batch series." -msgstr "" +msgstr "Šarža nije kreirana za artikal {0} jer nema Broj Šarže." #. Description of the 'Automatically Create New Batch' (Check) field in DocType #. 'Item' @@ -8765,7 +8765,7 @@ msgstr "Proknjižena Osnovna Imovina" #: erpnext/accounts/services/gl_validator.py:143 msgid "Books have been closed until the period ending on {0}" -msgstr "" +msgstr "Knjigovodstvo je zatvoreno do perioda koji se završava {0}" #. Option for the 'Type of Transaction' (Select) field in DocType 'Inventory #. Dimension' @@ -8993,7 +8993,7 @@ msgstr "Proračun se ne može dodijeliti naspram Grupnog Računu {0}" #: erpnext/accounts/doctype/budget/budget.py:165 msgid "Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense" -msgstr "" +msgstr "Proračun se ne može dodijeliti za {0}, jer njegova kontna Klasa nije Prihod ili Rashod" #: erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py:9 msgid "Budgets" @@ -9347,7 +9347,7 @@ msgstr "Izračunata Razlika Popusta" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:95 msgid "Calculating arrival times" -msgstr "" +msgstr "Izračunavanje vremena dolaska" #. Label of the section_break_11 (Section Break) field in DocType 'Supplier #. Scorecard Period' @@ -9565,7 +9565,7 @@ msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije na #: erpnext/stock/doctype/stock_settings/stock_settings.py:191 msgid "Can't change the valuation method, as there are transactions against some items which do not have their own valuation method" -msgstr "" +msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije naspram nekih artikala koji nemaju metod vrijednovanja" #: erpnext/support/doctype/warranty_claim/warranty_claim.py:79 msgid "Cancel Material Visit {0} before cancelling this Warranty Claim" @@ -9644,7 +9644,7 @@ msgstr "Ne može biti artikal fiksne imovine jer je kreiran Registar Zaliha." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:92 #: erpnext/stock/doctype/delivery_trip/delivery_trip.py:219 msgid "Cannot calculate arrival time as the driver address is missing." -msgstr "" +msgstr "Ne može se izračunati vrijeme dolaska jer nedostaje adresa vozača." #: erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py:117 msgid "Cannot cancel Asset Depreciation Schedule {0} as it has a draft journal entry {1}." @@ -9656,7 +9656,7 @@ msgstr "Ne može se otkazati Unos Zatvaranja Kase" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:140 msgid "Cannot cancel Stock Reservation Entry {0}, as it has been used in the work order {1}. Please cancel the work order first or unreserve the stock" -msgstr "" +msgstr "Ne može se otkazati Unos Rezervacije Zaliha {0}, jer je korišten u radnom nalogu {1}. Molimo prvo otkazati radni nalog ili otkloniti rezervaciju zaliha" #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:275 msgid "Cannot cancel as processing of cancelled documents is pending." @@ -9708,7 +9708,7 @@ msgstr "Nije moguće promijeniti standard valutu poduzeća, jer postoje postoje #: erpnext/projects/doctype/task/task.py:146 msgid "Cannot complete task {0} as its dependent task {1} is not completed / cancelled." -msgstr "" +msgstr "Ne može završiti zadatak {0} jer njegov zavisni zadatak {1} nije dovršen/poništen." #: erpnext/accounts/doctype/cost_center/cost_center.py:61 msgid "Cannot convert Cost Center to ledger as it has child nodes" @@ -9745,7 +9745,7 @@ msgstr "Nije moguće kreirati knjigovodstvene unose naspram onemogućenih račun #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:146 msgid "Cannot create more Subcontracting Orders against the Purchase Order {0}." -msgstr "" +msgstr "Ne može se stvoriti više Podugovornih Naloga na osnovu Naloga Nabave {0}." #: erpnext/controllers/sales_and_purchase_return.py:444 msgid "Cannot create return for consolidated invoice {0}." @@ -9840,7 +9840,7 @@ msgstr "Nije moguće spojiti {0} '{1}' u '{2}' jer oba imaju postojeće knjigovo #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:125 msgid "Cannot optimize route as the driver address is missing." -msgstr "" +msgstr "Ne može se optimizirati ruta jer nedostaje adresa vozača." #: erpnext/manufacturing/doctype/work_order/services/status.py:41 msgid "Cannot produce more Item {0} than Sales Order quantity {1} {2}" @@ -9870,7 +9870,7 @@ msgstr "Ne može se upućivati na broj reda veći ili jednak trenutnom broju red #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:626 msgid "Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.

The Allowed Qty is calculated as follows:
" -msgstr "" +msgstr "Ne može se rezervisati više od Dozvoljene Količine {0} {1} za artikal {2} prema {3} {4}.

Dozvoljena Količina se izračunava na sljedeći način:
" #: erpnext/accounts/doctype/bank/bank.js:63 msgid "Cannot retrieve link token for update. Check Error Log for more information" @@ -9895,7 +9895,7 @@ msgstr "Nije moguće odabrati tip naknade kao 'Iznos na Prethodnom Redu' ili 'Uk #: erpnext/stock/doctype/item_alternative/item_alternative.py:36 msgid "Cannot set alternative item for the item {0}" -msgstr "" +msgstr "Ne može se postaviti alternativni artikal za artikal. {0}" #: erpnext/selling/doctype/quotation/quotation.py:293 msgid "Cannot set as Lost as Sales Order is made." @@ -10325,7 +10325,7 @@ msgstr "Ručno promijenite ovaj datum da postavite sljedeći datum početka sinh #: erpnext/selling/doctype/customer/customer.py:161 msgid "Changed customer name to '{0}' as '{1}' already exists." -msgstr "" +msgstr "Ime klijenta je promijenjeno u '{0}' jer '{1}' već postoji." #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:160 msgid "Changes in {0}" @@ -10615,7 +10615,7 @@ msgstr "Podređena tabela nije dozvoljena" #: erpnext/projects/doctype/task/task.py:319 msgid "Child Task exists for this Task. You cannot delete this Task." -msgstr "" +msgstr "Podređeni Zadatak postoji za ovaj Zadatak. Ne možete izbrisati ovaj Zadatak." #: erpnext/stock/doctype/warehouse/warehouse_tree.js:21 msgid "Child nodes can be only created under 'Group' type nodes" @@ -11795,11 +11795,11 @@ msgstr "Naziv polja za link poduzeća koji se koristi za filtriranje (opciono - #: erpnext/setup/doctype/company/company.js:239 msgid "Company name does not match" -msgstr "" +msgstr "Naziv poduzeća ne odgovara" #: erpnext/assets/doctype/asset/asset.py:330 msgid "Company of asset {0} and purchase document {1} does not match." -msgstr "" +msgstr "Poduzeće imovine {0} i nabavni dokument {1} ne odgovara." #: erpnext/setup/doctype/employee/employee.py:164 msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" @@ -11839,11 +11839,11 @@ msgstr "Poduzeće {0} ne postoji" #: erpnext/setup/setup_wizard/operations/taxes_setup.py:14 msgid "Company {0} does not exist yet. Taxes setup aborted." -msgstr "" +msgstr "Poduzeće {0} još ne postoji. Postavljanje Pdv-a je prekinuto." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:579 msgid "Company {0} does not match with POS Profile Company {1}" -msgstr "" +msgstr "Poduzeće {0} nije usklađeno s Kasa Profilom {1}" #: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:105 msgid "Company {0} is added more than once" @@ -12319,7 +12319,7 @@ msgstr "Potrošena Količina" #: erpnext/manufacturing/doctype/work_order/services/reservation.py:186 msgid "Consumed Qty {0} cannot be greater than Reserved Qty {1} for item {2}" -msgstr "" +msgstr "Potrošena Količina {0} ne može biti veća od Rezervisane Količine {1} za artikal {2}" #. Label of the consumed_quantity (Data) field in DocType 'Asset Repair #. Consumed Item' @@ -13052,11 +13052,11 @@ msgstr "Centar Troškova {0} ne može se koristiti za dodjelu jer se koristi kao #: erpnext/assets/doctype/asset/asset.py:358 msgid "Cost Center {0} does not belong to Company {1}" -msgstr "" +msgstr "Centar Troškova {0} ne pripada {1}" #: erpnext/assets/doctype/asset/asset.py:365 msgid "Cost Center {0} is a group cost center and group cost centers cannot be used in transactions" -msgstr "" +msgstr "Centar Troškova {0} je grupni centar troškova a grupni centri troškova ne mogu se koristiti u transakcijama" #: erpnext/accounts/report/financial_statements.py:685 msgid "Cost Center: {0} does not exist" @@ -13181,7 +13181,7 @@ msgstr "Obračun Troškova i Fakturisanje" #: erpnext/projects/doctype/project/project.js:140 msgid "Costing and Billing fields have been updated" -msgstr "" +msgstr "Polja Troškova i Fakturisanje su ažurirana" #: erpnext/setup/demo.py:78 msgid "Could Not Delete Demo Data" @@ -13210,7 +13210,7 @@ msgstr "Nije moguće pronaći odgovarajuću promjenu koja bi odgovarala razlici: #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:46 #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:50 msgid "Could not find path for {0}" -msgstr "" +msgstr "Nije moguće pronaći putanju za {0}" #: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:101 msgid "Could not re-extract the table." @@ -14335,7 +14335,7 @@ msgstr "Trenutna Sastavnica" #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py:80 msgid "Current BOM and New BOM cannot be the same" -msgstr "" +msgstr "Trenutna i Nova Sastavnica ne mogu biti iste" #. Label of the current_exchange_rate (Float) field in DocType 'Exchange Rate #. Revaluation Account' @@ -17059,11 +17059,11 @@ msgstr "Račun Razlike u Postavkama Artikla" #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:155 msgid "Difference Account must be an Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" -msgstr "" +msgstr "Razlika u računu mora biti tip računa Imovine/Obaveza (Privremeno Početno), budući da je ovaj unos zaliha početni unos" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:985 msgid "Difference Account must be an Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" -msgstr "" +msgstr "Račun razlike mora biti račun tipa Imovina/Obaveze, budući da je ovo usklađivanje Zaliha Početni Unos" #. Label of the difference_amount (Currency) field in DocType 'Payment #. Reconciliation Allocation' @@ -17309,7 +17309,7 @@ msgstr "Onemogućeni artikli se ne mogu odabrati ni u jednoj transakciji." #: erpnext/accounts/services/internal_transfer.py:120 msgid "Disabled pricing rules since this {0} is an internal transfer" -msgstr "" +msgstr "Pravila određivanja cijena su onemogućena jer je ovo {0} interni prijenos" #. Description of the 'Disabled' (Check) field in DocType 'Supplier' #: erpnext/buying/doctype/supplier/supplier.json @@ -17318,7 +17318,7 @@ msgstr "Onemogućeni dobavljači su skriveni od odabira u novim transakcijama, a #: erpnext/accounts/services/internal_transfer.py:136 msgid "Disabled tax included prices since this {0} is an internal transfer" -msgstr "" +msgstr "Cijene bez PDV-a budući da je ovo {0} interni prijenos" #: erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py:82 msgid "Disabled template must not be default template" @@ -17569,7 +17569,7 @@ msgstr "Popust mora biti manji od 100%" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:3090 msgid "Discount of {0} applied as per Payment Term" -msgstr "" +msgstr "Popust od {0} primjenjen prema Uslovima Plaćanja" #. Label of the section_break_18 (Section Break) field in DocType 'Pricing #. Rule' @@ -17934,7 +17934,7 @@ msgstr "Želiš li podnijeti unos zaliha?" #: erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py:50 #: erpnext/selling/report/sales_partner_commission_summary/test_sales_partner_commission_summary.py:22 msgid "DocType can be one of {0}" -msgstr "" +msgstr "DocType može biti jedan od {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:182 #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:458 @@ -18659,7 +18659,7 @@ msgstr "Verifikacija e-pošte nije uspjela." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:20 msgid "Emails queued" -msgstr "" +msgstr "E-pošta u redu čekanja" #. Label of the emergency_contact_details (Section Break) field in DocType #. 'Employee' @@ -18938,7 +18938,7 @@ msgstr "Omogući Evropski Pristup" #. 'CRM Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Enable Frappe CRM Data Synchronization" -msgstr "" +msgstr "Omogući sinhronizaciju podataka Prodajne Podrške" #. Label of the enable_fuzzy_matching (Check) field in DocType 'Accounts #. Settings' @@ -19397,7 +19397,7 @@ msgstr "Unesi {0} iznos." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:170 msgid "Enter {0} name." -msgstr "" +msgstr "Unesi {0} ime." #: erpnext/setup/setup_wizard/data/industry_type.txt:22 msgid "Entertainment & Leisure" @@ -19496,15 +19496,15 @@ msgstr "Greška prilikom ponovnog knjiženja vrijednosti artikla" #: erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py:175 msgid "Error: This asset already has {0} depreciation periods booked. The `depreciation start` date must be at least {1} periods after the `available for use` date. Please correct the dates accordingly." -msgstr "" +msgstr "Greška: Ova imovina već ima uknjiženih {0} perioda amortizacije. Datum `početka amortizacije` mora biti najmanje {1} perioda nakon datuma `dostupno za upotrebu`. Molimo vas da ispravite datume u skladu s tim." #: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py:93 msgid "Error: {0}" -msgstr "" +msgstr "Greška: {0}" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:980 msgid "Error: {0} is a mandatory field" -msgstr "" +msgstr "Greška: {0} je obavezno polje" #. Label of the errors_notification_section (Section Break) field in DocType #. 'Stock Reposting Settings' @@ -20197,7 +20197,7 @@ msgstr "Neuspješni Unosi" #: erpnext/utilities/doctype/video_settings/video_settings.py:35 msgid "Failed to authenticate the API key. Please check the error logs." -msgstr "" +msgstr "Autentifikacija API ključa nije uspjela. Molimo provjerite zapise o greškama." #: erpnext/setup/setup_wizard/setup_wizard.py:37 #: erpnext/setup/setup_wizard/setup_wizard.py:38 @@ -21176,11 +21176,11 @@ msgstr "Za Radni Nalog" #: erpnext/controllers/status_updater.py:292 msgid "For an item {0}, quantity must be a negative number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti negativan broj" #: erpnext/controllers/status_updater.py:289 msgid "For an item {0}, quantity must be a positive number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti pozitivan broj" #. Description of the 'Income Account' (Link) field in DocType 'Dunning' #: erpnext/accounts/doctype/dunning/dunning.json @@ -21214,11 +21214,11 @@ msgstr "Za individualnog Dobavljača" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:379 msgid "For item {0}, only {1} assets have been created or linked to {2}. Please create or link {3} more assets with the respective document." -msgstr "" +msgstr "Za artikal {0}, samo {1} imovina je stvorena ili povezana s {2}. Stvori ili poveži još {3} imovine s odgovarajućim dokumentom." #: erpnext/controllers/status_updater.py:302 msgid "For item {0}, rate must be a positive number. To allow negative rates, enable {1} in {2}" -msgstr "" +msgstr "Za artikal {0}, cijena mora biti pozitivan broj. Da biste omogućili negativne cijene, omogućite {1} u {2}" #. Description of the 'Do not fetch incoming rate from Serial No' (Check) field #. in DocType 'Stock Reposting Settings' @@ -21232,7 +21232,7 @@ msgstr "Za operaciju {0} u redu {1}, molimo dodajte sirovine ili postavite Sasta #: erpnext/manufacturing/doctype/work_order/mapper.py:379 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity ({2})" -msgstr "" +msgstr "Za Operaciju {0}: Količina ({1}) ne može biti veća od količine na čekanju ({2})" #: erpnext/projects/doctype/project/project.js:208 msgid "For project - {0}, update your status" @@ -21278,7 +21278,7 @@ msgstr "Za praktičnost Klienta, ovi kodovi se mogu koristiti u formatima za isp #: erpnext/stock/serial_batch_bundle.py:1234 msgid "For the item {0}, the Available qty {1} is less than the Required Qty {2} in the warehouse {3}. Please add sufficient qty in the warehouse." -msgstr "" +msgstr "Za artikal {0}, Dostupna količina {1} je manja od Potrebne količine {2} u skladištu {3}. Dodaj dovoljnu količinu u skladište." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:893 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." @@ -21381,11 +21381,11 @@ msgstr "Podrška Prodaje" #. Name of a DocType #: erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json msgid "Frappe CRM Allowed User" -msgstr "" +msgstr "Dozvoljeni korisnik Prodajne Podrške" #: erpnext/crm/frappe_crm_api.py:168 msgid "Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Sinhronizacija podataka Prodajne Podrške nije omogućena na Sistemu. Kontaktiraj Odgovornog Sistema." #: erpnext/setup/install.py:232 msgid "Frappe School" @@ -22057,7 +22057,7 @@ msgstr "Dužina napomena Knjigovodstvenog Registra" #: erpnext/accounts/report/general_ledger/general_ledger.py:829 msgid "General Ledger requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Knjigovodstveni Registar zahtijeva da se {0} sinhronizira sa DuckDB-om" #. Label of the gs (Section Break) field in DocType 'Item Group' #: erpnext/setup/doctype/item_group/item_group.json @@ -24098,7 +24098,7 @@ msgstr "Uvezi Fakture" #. Import' #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json msgid "Import MT940 Format" -msgstr "" +msgstr "Uvezi MT940 Format" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:144 msgid "Import Successful" @@ -25495,7 +25495,7 @@ msgstr "Nevažeće Skladište" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:460 msgid "Invalid amount in accounting entries of {0} {1} for Account {2}: {3}" -msgstr "" +msgstr "Nevažeći iznos u knjigovodstvenim unosima {0} {1} za račun {2}: {3}" #: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:312 msgid "Invalid condition expression" @@ -27846,7 +27846,7 @@ msgstr "Artikal {0} se nemože naručiti više od {1} u odnosu na Ugovorni Nalog #: erpnext/stock/services/internal_transfer.py:104 msgid "Item {0} cannot be received in more than {1} qty against the {2} {3}" -msgstr "" +msgstr "Artikal {0} ne može biti primljen u količini većoj od {1} u odnosu na {2} {3}" #: erpnext/assets/doctype/asset/asset.py:343 #: erpnext/stock/doctype/item/item.py:693 @@ -27892,7 +27892,7 @@ msgstr "Artikal {0} zanemaren jer nije artikal na zalihama" #: erpnext/stock/get_item_details.py:359 msgid "Item {0} is a template, please select one of its variants" -msgstr "" +msgstr "Artikal {0} je šablon, molimo odaberite jednu od njenih varijanti" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:614 msgid "Item {0} is already reserved/delivered against Sales Order {1}." @@ -28010,7 +28010,7 @@ msgstr "Artikal: {0} ne postoji u sistemu" #: erpnext/manufacturing/doctype/bom/bom.py:970 msgid "Item: {0} with Stock UOM: {1} cannot have fractional process loss qty as UOM {2} is a whole number." -msgstr "" +msgstr "Artikal: {0} sa Jedinicom Zalihe: {1} ne može imati količinu frakcijskog gubitka procesa jer je jedinica mjere {2} cijeli broj." #. Label of a Card Break in the Buying Workspace #. Label of a Workspace Sidebar Item @@ -28217,7 +28217,7 @@ msgstr "Radne Kartice {0} je završen" #: erpnext/manufacturing/doctype/job_card/job_card.py:1416 msgid "Job Card {0}: As per the sequence of the operations in the work order {1}, complete the operation {2} before the operation {3}." -msgstr "" +msgstr "Radna Kartica {0}: Prema redoslijedu operacija u radnom nalogu {1}, dovršite operaciju {2} prije operacije {3}." #. Label of the dashboard_tab (Tab Break) field in DocType 'Workstation' #: erpnext/manufacturing/doctype/workstation/workstation.json @@ -28292,11 +28292,11 @@ msgstr "Radna Kartica {0} kreirana" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:106 msgid "Job paused" -msgstr "" +msgstr "Posao pauziran" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:64 msgid "Job started" -msgstr "" +msgstr "Posao započet" #: erpnext/utilities/bulk_transaction.py:72 msgid "Job: {0} has been triggered for processing failed transactions" @@ -28645,7 +28645,7 @@ msgstr "Prošla Fiskalna Godina" #: erpnext/accounts/doctype/account/account.py:673 msgid "Last GL Entry update was done {0}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying." -msgstr "" +msgstr "Posljednje ažuriranje Knjigovodstvenog Registra je obavljeno {0}. Ova operacija nije dozvoljena dok se sistem aktivno koristi. Pričekaj 5 minuta prije ponovnog pokušaja." #. Label of the last_integration_date (Date) field in DocType 'Bank Account' #: erpnext/accounts/doctype/bank_account/bank_account.json @@ -29163,7 +29163,7 @@ msgstr "Povezivanje s klijentom nije uspjelo. Molimo pokušajte ponovo." #: erpnext/selling/doctype/customer/customer.js:282 msgid "Linking to Supplier failed. Please try again." -msgstr "" +msgstr "Povezivanje s Dobavljačem nije uspjelo. Pokušaj ponovo." #: erpnext/accounts/report/financial_ratios/financial_ratios.js:55 #: erpnext/accounts/report/financial_ratios/financial_ratios.py:152 @@ -30731,7 +30731,7 @@ msgstr "Materijali su već primljeni naspram {0} {1}" #: erpnext/manufacturing/doctype/job_card/job_card.py:189 #: erpnext/manufacturing/doctype/job_card/job_card.py:903 msgid "Materials need to be transferred to the work in progress warehouse for the job card {0}" -msgstr "" +msgstr "Materijali se moraju prenijeti u skladište nedovršene proizvodnje za radnu karticu {0}" #. Label of the max_amount (Currency) field in DocType 'Bank Transaction Rule' #. Label of the max_amount (Currency) field in DocType 'Promotional Scheme @@ -31618,7 +31618,7 @@ msgstr "Više Računa (Šablon Naloga Knjiženja)" #: erpnext/selling/doctype/customer/customer.py:443 msgid "Multiple Loyalty Programs found for Customer {0}. Please select manually." -msgstr "" +msgstr "Višestruki Programi Lojalnosti su pronađeni za Klijenta {0}. Odaberi ručno." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:253 msgid "Multiple POS Opening Entry" @@ -31626,7 +31626,7 @@ msgstr "Višestruki Unos Otvaranja Kase" #: erpnext/accounts/doctype/pricing_rule/utils.py:345 msgid "Multiple Price Rules exist with same criteria, please resolve conflict by assigning priority. Price Rules: {0}" -msgstr "" +msgstr "Postoji više pravila za cijene s istim kriterijima, riješi sukob dodjeljivanjem prioriteta. Pravila Cijena: {0}" #. Option for the 'Loyalty Program Type' (Select) field in DocType 'Loyalty #. Program' @@ -32289,7 +32289,7 @@ msgstr "Novi Radni Prostor" #: erpnext/selling/doctype/customer/customer.py:408 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be at least {0}" -msgstr "" +msgstr "Novo kreditno ograničenje je niže od trenutnog iznosa klijenta. Kreditno ograničenje mora biti najmanje {0}" #. Description of the 'Bill Even If Previous Invoice Unpaid' (Check) field in #. DocType 'Subscription' @@ -32299,7 +32299,7 @@ msgstr "Nove fakture će se generirati prema rasporedu čak i ako su trenutne fa #: erpnext/support/doctype/issue/issue.js:126 msgid "New issue created: {0}" -msgstr "" +msgstr "Novi zahtjev stvoren: {0}" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:261 msgid "New release date should be in the future" @@ -32383,7 +32383,7 @@ msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:146 msgid "No Delivery Note selected for Customer {0}" -msgstr "" +msgstr "Nije odabrana Dostavnica za Klijenta {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:767 msgid "No DocTypes in To Delete list. Please generate or import the list before submitting." @@ -32548,7 +32548,7 @@ msgstr "Nisu pronađeni kontakti s e-poštom." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:164 msgid "No customers found with selected options." -msgstr "" +msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/selling/page/sales_funnel/sales_funnel.js:137 msgid "No data for this period" @@ -32769,7 +32769,7 @@ msgstr "Nije pronađen nijedan zapis" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:22 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:39 msgid "No records for these settings." -msgstr "" +msgstr "Nema zapisa za ove postavke." #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:773 msgid "No records found in Allocation table" @@ -33268,7 +33268,7 @@ msgstr "Numeričke Vrijednosti" #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py:88 msgid "Numero has not been set in the XML file" -msgstr "" +msgstr "Broj nije postavljen u XML datoteci" #. Option for the 'Blood Group' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json @@ -33444,11 +33444,11 @@ msgstr "Nakon postavljanja, ova faktura će biti na čekanju do postavljenog dat #: erpnext/manufacturing/doctype/work_order/work_order.js:763 msgid "Once the Work Order is Closed, it cannot be resumed." -msgstr "" +msgstr "Nakon što je Radni Nalog Zatvoren. Ne može se ponovo otvoriti." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:39 msgid "One customer can be part of only a single Loyalty Program." -msgstr "" +msgstr "Jedan Klijent može biti dio samo jednog Programa Lojalnosti." #. Option for the 'Status' (Select) field in DocType 'Subcontracting Inward #. Order' @@ -34045,7 +34045,7 @@ msgstr "Operacija {0} ne pripada radnom nalogu {1}" #: erpnext/manufacturing/doctype/workstation/workstation.py:453 msgid "Operation {0} is longer than any available working hours in workstation {1}, break down the operation into multiple operations" -msgstr "" +msgstr "Operacija {0} traje duže od bilo kojeg raspoloživog radnog vremena na radnoj stanici {1}, podijeli operaciju na više operacija" #. Label of the operations (Table) field in DocType 'BOM' #. Label of the operations_section_section (Section Break) field in DocType @@ -34232,7 +34232,7 @@ msgstr "Optimiziraj Rutu" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:128 msgid "Optimizing route" -msgstr "" +msgstr "Optimizacija rute" #: erpnext/manufacturing/doctype/work_order/work_order.js:1029 msgid "Optional. Select a specific manufacture entry to reverse." @@ -34693,7 +34693,7 @@ msgstr "Preko Odbitka" #: erpnext/accounts/services/billing_validation.py:56 msgid "Overbilling of {0} ignored because you have {1} role." -msgstr "" +msgstr "Prekomjerno Fakturisanje {0} zanemareno jer imate {1} ulogu." #: erpnext/controllers/status_updater.py:519 msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." @@ -34815,7 +34815,7 @@ msgstr "Verifikat Zatvaranje Perioda" #. Label of the pcv_job_timeout (Int) field in DocType 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "PCV Job Timeout (seconds)" -msgstr "" +msgstr "Vremensko Ograničenje Zadatka Završnog Verifikata Perioda (sekunde)" #: erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js:35 msgid "PCV Paused" @@ -34963,7 +34963,7 @@ msgstr "Kasa Faktura nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:130 msgid "POS Invoice isn't created by user {0}" -msgstr "" +msgstr "Korisnik {0} nije stvorio Kasa Fakturu" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:208 msgid "POS Invoice should have the field {0} checked." @@ -35087,7 +35087,7 @@ msgstr "Korisnik Kasa Profila" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:124 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:189 msgid "POS Profile doesn't match {0}" -msgstr "" +msgstr "Kasa Profil ne poklapa se s {0}" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:210 msgid "POS Profile is mandatory to mark this invoice as POS Transaction." @@ -35099,19 +35099,19 @@ msgstr "Kasa profil {0} ne može biti onemogućen jer su Kasa sesije u toku." #: erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py:62 msgid "POS Profile {0} contains Mode of Payment {1}. Please remove them to disable this mode." -msgstr "" +msgstr "Kasa Profil {0} sadrži ovaj način plaćanja {1}. Uklonite ga da onemogućite ovaj način." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:58 msgid "POS Profile {0} does not belong to company {1}" -msgstr "" +msgstr "Kasa Profil {0} ne pripada {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:47 msgid "POS Profile {0} does not exist." -msgstr "" +msgstr "Kasa Profil {0} ne postoji." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:54 msgid "POS Profile {0} is disabled." -msgstr "" +msgstr "Kasa Profil {0} je onemogućen." #. Name of a report #: erpnext/accounts/report/pos_register/pos_register.json @@ -36059,7 +36059,7 @@ msgstr "Stranka je Obavezna" #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:69 msgid "Party is required to create a payment entry." -msgstr "" +msgstr "Stranka je obavezna za stvaranje unosa plaćanja." #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:66 msgid "Party type is required to create a payment entry." @@ -36766,7 +36766,7 @@ msgstr "Tip Plaćanja" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:624 msgid "Payment Type must be one of Receive, Pay, or Internal Transfer" -msgstr "" +msgstr "Tip Plaćanja mora biti Uplata, Isplata ili Interni Prijenos" #. Label of the payment_url (Data) field in DocType 'Payment Request' #: erpnext/accounts/doctype/payment_request/payment_request.json @@ -37723,7 +37723,7 @@ msgstr "Dodaj račun za pravilo bankovnog unosa." #: erpnext/public/js/utils/serial_no_batch_selector.js:663 msgid "Please add at least one Serial No / Batch No" -msgstr "" +msgstr "Dodaj barem jedan Serijski / Šaržni Broj" #: erpnext/stock/doctype/item/item.js:914 msgid "Please add at least one row in Item Defaults with a Company before setting opening stock." @@ -37731,7 +37731,7 @@ msgstr "Dodaj barem jedan red u Postavke Artikala sa poduzećem prije postavljan #: erpnext/crm/doctype/crm_settings/crm_settings.py:51 msgid "Please add at least one user on Allowed Users to allow Data Synchronization from Frappe CRM site." -msgstr "" +msgstr "Dodaj barem jednog korisnika na listu Dozvoljeni Korisnici kako biste omogućili sinhronizaciju podataka sa Prodajnom Podrškom." #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py:84 msgid "Please add the Bank Account column" @@ -37823,7 +37823,7 @@ msgstr "Konfiguriraj račune za pravilo bankovnog unosa." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:345 msgid "Please contact any of the following users for this transaction." -msgstr "" +msgstr "Kontaktiraj bilo kojeg od sljedećih korisnika za ovu transakciju." #: erpnext/selling/doctype/customer/customer.py:534 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" @@ -37895,7 +37895,7 @@ msgstr "Omogući {0} u {1}." #: erpnext/controllers/selling_controller.py:872 msgid "Please enable {0} in {1} to allow same item in multiple rows" -msgstr "" +msgstr "Omogući {0} u {1} kako biste dozvolili isti artikal u više redova" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:378 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." @@ -37907,11 +37907,11 @@ msgstr "Potvrdi da je {0} račun {1} Troškovni račun. Možete promijeniti vrst #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:758 msgid "Please ensure {0} account is a Balance Sheet account." -msgstr "" +msgstr "Provjeri da li je račun {0} račun Bilansa Stanja." #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:768 msgid "Please ensure {0} account {1} is a Receivable account." -msgstr "" +msgstr "Provjeri da li je {0} račun {1} račun Potraživanja." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:140 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" @@ -38118,7 +38118,7 @@ msgstr "Molimo vas da generirate listu za brisanje prije podnošenja" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:67 msgid "Please import accounts against parent company or enable {0} in company master." -msgstr "" +msgstr "Uvezi račune naspram matičnog poduzeća ili omogući {0} u Postavkama Poduzeća." #: erpnext/setup/doctype/employee/employee.py:291 msgid "Please make sure the employees above report to another Active employee." @@ -38216,7 +38216,7 @@ msgstr "Odaberi Poduzeće" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:139 #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:76 msgid "Please select Company and Posting Date to get entries" -msgstr "" +msgstr "Odaberi Poduzeće i Datum Knjiženja da biste preuzeli unose" #: erpnext/accounts/doctype/journal_entry/journal_entry.js:435 #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:28 @@ -38249,7 +38249,7 @@ msgstr "Odaberi Kod Artikla" #: erpnext/selling/doctype/sales_order/sales_order.js:1756 msgid "Please select Items from the Table" -msgstr "" +msgstr "Odaberi Artikle iz Tabele" #: erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py:55 msgid "Please select Maintenance Status as Completed or remove Completion Date" @@ -38398,7 +38398,7 @@ msgstr "Odaberi red za kreiranje Unosa Ponovnog Knjiženje" #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:146 msgid "Please select a supplier" -msgstr "" +msgstr "Odaberi Dobavljača" #: erpnext/accounts/report/purchase_register/purchase_register.py:37 msgid "Please select a supplier for fetching payments." @@ -38410,7 +38410,7 @@ msgstr "Odaberi važeći Nabavni Nalog koji je konfigurisan za Podizvođača." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:200 msgid "Please select a valid document type." -msgstr "" +msgstr "Odaberi važeći tip dokumenta." #: erpnext/selling/doctype/quotation/quotation.js:245 msgid "Please select a value for {0} quotation_to {1}" @@ -38430,7 +38430,7 @@ msgstr "Odaberi barem jedan filter: Šifra Artikla, Šarža ili Serijski Broj." #: erpnext/selling/doctype/sales_order/sales_order.js:1368 msgid "Please select at least one item to continue" -msgstr "" +msgstr "Odaberi jedan artikal za nastavak" #: erpnext/buying/doctype/purchase_order/services/drop_ship.py:17 msgid "Please select at least one item to update delivered quantity." @@ -38438,7 +38438,7 @@ msgstr "Molimo odaberite barem jedan artikal za ažuriranje isporučene količin #: erpnext/manufacturing/doctype/work_order/work_order.js:392 msgid "Please select at least one operation to create Job Card" -msgstr "" +msgstr "Odaberi barem jednu operaciju za stvaranje Kartice Posla" #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js:33 msgid "Please select at least one row to fix" @@ -38504,7 +38504,7 @@ msgstr "Odaberi Poduzeće" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:65 msgid "Please select the Multiple Tier Program type for more than one collection rule." -msgstr "" +msgstr "Odaberi Tip Višeslojnog Programa za više od jednog pravila prikupljanja." #: erpnext/stock/doctype/item/item.js:433 msgid "Please select the Warehouse first" @@ -38566,7 +38566,7 @@ msgstr "Postavi Račun u Skladištu {0} ili Standard Račun Zaliha u {1}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:333 msgid "Please set Accounting Dimension {0} in {1}" -msgstr "" +msgstr "Postavi Knjigovodstvenu Dimenziju {0} u {1}" #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:23 #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:34 @@ -38608,7 +38608,7 @@ msgstr "Postavi Račun Osnovne Imovine u Kategoriju Imovine {0}" #: erpnext/accounts/doctype/purchase_invoice/services/expense_account.py:151 msgid "Please set Fixed Asset Account in {0} against {1}." -msgstr "" +msgstr "Postavi Račun Osnovnih Sredstava u {0} na {1}." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:296 msgid "Please set Parent Row No for item {0}" @@ -38645,7 +38645,7 @@ msgstr "Postavi Poduzeće" #: erpnext/assets/doctype/asset/asset.py:374 msgid "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {0}" -msgstr "" +msgstr "Postavi Centar Troškova za Imovinu ili postavite Centar Troškova Amortizacije za {0}" #: erpnext/stock/doctype/item/item.py:339 #: erpnext/stock/doctype/item/item.py:1623 @@ -38699,11 +38699,11 @@ msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Način Plaćanja {0}" #: erpnext/accounts/doctype/pos_profile/pos_profile.py:207 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:367 msgid "Please set default Cash or Bank account in Mode of Payments {0}" -msgstr "" +msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Načine Plaćanja {0}" #: erpnext/accounts/utils.py:2568 msgid "Please set default Exchange Gain/Loss Account in Company {0}" -msgstr "" +msgstr "Postavi Standard Račun Rezultata od Kursnih Razlika u {0}" #: erpnext/assets/doctype/asset_repair/services/gl_composer.py:92 msgid "Please set default Expense Account in Company {0}" @@ -38842,11 +38842,11 @@ msgstr "Navedi od/Do Raspona" #: erpnext/public/js/controllers/transaction.js:2634 msgid "Please specify {0}. It is needed to fetch Item Details." -msgstr "" +msgstr "Navedi {0}. Potrebno je za preuzimanje Detalja Artikla." #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:142 msgid "Please submit Purchase Order {0} before proceeding." -msgstr "" +msgstr "Podnesite Nalog Nabave {0} prije nego što nastavite." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:276 msgid "Please try again in an hour." @@ -39080,7 +39080,7 @@ msgstr "Datum Knjiženja" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:263 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:146 msgid "Posting Date cannot be a future date" -msgstr "" +msgstr "Datum knjiženja ne može biti budući datum" #. Label of the exchange_gain_loss_posting_date (Select) field in DocType #. 'Accounts Settings' @@ -39283,7 +39283,7 @@ msgstr "Uplaćeni Troškovi" #: erpnext/accounts/report/general_ledger/general_ledger.py:682 msgid "Presentation Currency cannot be {0}, when {1} is enabled." -msgstr "" +msgstr "Valuta prikaza ne može biti {0}, kada je omogućen {1}." #: erpnext/setup/setup_wizard/data/designation.txt:24 msgid "President" @@ -39957,7 +39957,7 @@ msgstr "Prioriteti" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:61 msgid "Priority cannot be less than 1." -msgstr "" +msgstr "Prioritet ne može biti manji od 1." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:764 msgid "Priority has been changed to {0}." @@ -40523,7 +40523,7 @@ msgstr "Bilans Uspjeha" #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:215 msgid "Profit and Loss Statement requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilansa Uspjeha zahtijeva da se {0} sinhronizira s DuckDB-om" #. Label of the heading_cppb (Heading) field in DocType 'Bisect Accounting #. Statements' @@ -41282,7 +41282,7 @@ msgstr "Nabavni Nalog Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:466 msgid "Purchase Order Required for item {0}" -msgstr "" +msgstr "Nabavni Nalog je obavezan za artikal {0}" #. Name of a report #. Label of a chart in the Buying Workspace @@ -41342,7 +41342,7 @@ msgstr "Nabavni Nalozi za Prijem" #: erpnext/controllers/accounts_controller.py:1236 msgid "Purchase Orders {0} are unlinked" -msgstr "" +msgstr "Nabavni Nalozi {0} nisu povezani" #: erpnext/stock/report/item_prices/item_prices.py:59 msgid "Purchase Price List" @@ -41432,7 +41432,7 @@ msgstr "Nabavni Račun je Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:488 msgid "Purchase Receipt Required for item {0}" -msgstr "" +msgstr "Nabavni Račun je obavezan za artikal {0}" #. Label of a Link in the Buying Workspace #. Name of a report @@ -41452,7 +41452,7 @@ msgstr "Statistika Nabavnog Računa " #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:356 msgid "Purchase Receipt does not have any Item for which Retain Sample is enabled." -msgstr "" +msgstr "Nabavni Račun nema nijedan artikal za koju je omogućeno Zadržavanje Uzorka." #: erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py:135 msgid "Purchase Receipt {0} created." @@ -42483,7 +42483,7 @@ msgstr "Količina za Skeniranje" #: erpnext/stock/doctype/stock_entry/stock_entry.py:910 msgid "Quantity {0} should not be greater than allowed quantity {1}" -msgstr "" +msgstr "Količina {0} ne smije biti veća od dozvoljene količine {1}" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json @@ -42935,7 +42935,7 @@ msgstr "PDV Stopa" #: erpnext/accounts/services/child_item_update.py:515 msgid "Rate of '{0}' items cannot be changed" -msgstr "" +msgstr "Cijena '{0}' artikala ne može se mijenjati" #. Label of the rate_of_depreciation (Percent) field in DocType 'Asset #. Depreciation Schedule' @@ -43674,7 +43674,7 @@ msgstr "Zabilježite prijenos između dva bankovna računa" #: erpnext/stock/doctype/item_alternative/item_alternative.py:84 msgid "Record already exists for the item {0}" -msgstr "" +msgstr "Zapis za artikal {0} već postoji" #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:513 #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:519 @@ -44099,7 +44099,7 @@ msgstr "Odbijeno Skladište" #: erpnext/public/js/utils/serial_no_batch_selector.js:671 msgid "Rejected Warehouse and Accepted Warehouse cannot be the same." -msgstr "" +msgstr "Odbijeno i Prihvaćeno Skladište ne mogu biti isto." #: erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py:23 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py:14 @@ -44528,11 +44528,11 @@ msgstr "Datoteke Podataka Ponovnog Knjiženja" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:47 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:96 msgid "Reposting Entries will change the value of accounts Stock In Hand, and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje će promijeniti vrijednost računa Zalihe na raspolaganju i Troškovi zaliha u izvještaju o probnom bilansu, a također će promijeniti i vrijednost stanja u izvještaju o stanju zaliha." #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:62 msgid "Reposting Entry will change the value of accounts Stock In Hand and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje će promijeniti vrijednost računa Zalihe na raspolaganju i Troškovi zaliha u izvještaju o probnom bilansu, a također će promijeniti i vrijednost stanja u izvještaju o stanju zaliha." #. Label of the reposting_info_section (Section Break) field in DocType 'Repost #. Item Valuation' @@ -44919,7 +44919,7 @@ msgstr "Rezervno Skladište" #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:178 msgid "Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {0}." -msgstr "" +msgstr "Rezervno Skladište mora biti različito od Dobavljačevog Skladišta za Isporučeni Artikal {0}." #: erpnext/manufacturing/doctype/production_plan/production_plan.js:287 msgid "Reserve for Raw Materials" @@ -45522,7 +45522,7 @@ msgstr "Povrati" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:359 msgid "Revaluation Journal: {0}" -msgstr "" +msgstr "Žurnal Revalorizacije: {0}" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:151 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:113 @@ -46038,7 +46038,7 @@ msgstr "Red #{0}: Broj Šarže {1} je već odabran." #: erpnext/controllers/subcontracting_inward_controller.py:443 msgid "Row #{0}: Batch No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)." -msgstr "" +msgstr "Red #{0}: Broj(evi) Šarže {1} nisu dio povezanog Internog Podugovaračkog Naloga. Odaberi važeći Broj(eve) Šarže." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:880 msgid "Row #{0}: Cannot allocate more than {1} against payment term {2}" @@ -46130,7 +46130,7 @@ msgstr "Red #{0}: Kumulativni prag ne može biti manji od praga pojedinačne tra #: erpnext/assets/doctype/asset_category/asset_category.py:66 msgid "Row #{0}: Currency of {1} - {2} does not match company currency." -msgstr "" +msgstr "Red #{0}: Valuta od {1} - {2} ne odgovara valuti poduzeća." #: erpnext/controllers/subcontracting_inward_controller.py:91 msgid "Row #{0}: Customer Provided Item {1} against Subcontracting Inward Order Item {2} ({3}) cannot be added multiple times." @@ -46184,7 +46184,7 @@ msgstr "Red #{0}: Duplikat unosa u Referencama {1} {2}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:113 msgid "Row #{0}: Either Party ID or Party Name is required" -msgstr "" +msgstr "Red #{0}: Obavezan je ili ID Stranke ili Naziv Stranke" #: erpnext/selling/doctype/sales_order/sales_order.py:270 msgid "Row #{0}: Expected Delivery Date cannot be before Purchase Order Date" @@ -46200,7 +46200,7 @@ msgstr "Red #{0}: Račun troškova {1} nije važeći za Nabavnu Fakturu {2}. Doz #: erpnext/assets/doctype/asset/asset.py:421 msgid "Row #{0}: Finance Book should not be empty since you're using multiple." -msgstr "" +msgstr "Red #{0}: Finansijski Registar ne smije biti prazan jer ih koristite više." #: erpnext/selling/doctype/sales_order/services/subcontracting.py:40 msgid "Row #{0}: Finished Good Item Qty can not be zero" @@ -46208,7 +46208,7 @@ msgstr "Red #{0}: Količina gotovog proizvoda artikla ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:39 msgid "Row #{0}: Finished Good Item Qty cannot be zero" -msgstr "" +msgstr "Red #{0}: Količina gotovog proizvoda ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:21 #: erpnext/selling/doctype/sales_order/services/subcontracting.py:20 @@ -46259,7 +46259,7 @@ msgstr "Red #{0}: Polja Od i Do su obavezna" #: erpnext/stock/doctype/pick_list/pick_list.py:650 msgid "Row #{0}: Item Code is Mandatory" -msgstr "" +msgstr "Red #{0}: Šifra Artikla je obavezna" #: erpnext/public/js/utils/barcode_scanner.js:427 msgid "Row #{0}: Item added" @@ -46316,15 +46316,15 @@ msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može s #: erpnext/controllers/subcontracting_inward_controller.py:80 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted, add another row instead." -msgstr "" +msgstr "Red #{0}: Artikal {1} ne odgovara. Promjena šifre artikla nije dozvoljena, umjesto toga dodaj još jedan red." #: erpnext/controllers/subcontracting_inward_controller.py:129 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted." -msgstr "" +msgstr "Red #{0}: Artikal {1} ne odgovara. Promjena šifre artikla nije dozvoljena." #: erpnext/stock/doctype/stock_entry/services/subcontracting.py:94 msgid "Row #{0}: Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" -msgstr "" +msgstr "Red #{0}: Artikal {1} nije pronađen u tabeli 'Dostavljene Sirovine' u {2} {3}" #: erpnext/stock/doctype/stock_entry/services/disassemble.py:115 msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." @@ -46365,19 +46365,19 @@ msgstr "Red #{0}: Prekomjerna potrošnja Klijent Dostavljenog Artikla {1} u odno #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:92 msgid "Row #{0}: POS Invoice {1} has been {2}" -msgstr "" +msgstr "Red #{0}: Kasa Faktura {1} je {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:73 msgid "Row #{0}: POS Invoice {1} is not against customer {2}" -msgstr "" +msgstr "Red #{0}: Kasa Faktura {1} nije vezana za klijenta {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:88 msgid "Row #{0}: POS Invoice {1} is not submitted yet" -msgstr "" +msgstr "Red #{0}: Kasa Faktura {1} još nije podnešena" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:123 msgid "Row #{0}: Party ID is required" -msgstr "" +msgstr "Red #{0}: ID Stranke je obavezan" #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:80 msgid "Row #{0}: Please select Item Code in Assembly Items" @@ -46385,11 +46385,11 @@ msgstr "Red #{0}: Odaberi Kod Artikla u Artiklima Montaže" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:340 msgid "Row #{0}: Please select a valid Quality Inspection with Item Code {1}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete sa Šifrom Artikla {1}." #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:333 msgid "Row #{0}: Please select a valid Quality Inspection with Reference Type {1} and Reference Name {2}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete s Tipom Reference {1} i Nazivom Reference {2}." #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:82 msgid "Row #{0}: Please select the BOM No in Assembly Items" @@ -46413,7 +46413,7 @@ msgstr "Red #{0}: Ažuriraj račun odloženih prihoda/troškova u redu artikla i #: erpnext/assets/doctype/asset/asset.py:413 msgid "Row #{0}: Please use a different Finance Book." -msgstr "" +msgstr "Red #{0}: Koristi drugi Finansijski Registar." #: erpnext/manufacturing/doctype/bom/bom.py:378 #, python-format @@ -46435,7 +46435,7 @@ msgstr "Red #{0}: Količina mora biti pozitivan broj" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:429 msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Item {2} against Batch {3} in Warehouse {4}." -msgstr "" +msgstr "Red #{0}: Količina bi trebala biti manja ili jednaka Dostupnoj Količini za Rezervaciju (Stvarna količina - Rezervisana količina) {1} za artikal {2} naspram Šarže {3} u Skladištu {4}." #: erpnext/stock/services/quality_inspection_service.py:77 msgid "Row #{0}: Quality Inspection is required for Item {1}" @@ -46512,7 +46512,10 @@ msgid "Row #{0}: Selling rate for item {1} is lower than its {2}.\n" "\t\t\t\t\tSelling {3} should be at least {4}.

Alternatively,\n" "\t\t\t\t\tyou can disable '{5}' in {6} to bypass\n" "\t\t\t\t\tthis validation." -msgstr "" +msgstr "Red #{0}: Prodajna cijena za artikal {1} je niža od njegove {2}.\n" +"\t\t\t\t\tProdaja {3} treba biti najmanje {4}.

Alternativno,\n" +"\t\t\t\t\tmožete onemogućiti '{5}' u {6} da biste zaobišli\n" +"\t\t\t\t\tovu validaciju." #: erpnext/manufacturing/doctype/work_order/work_order.py:348 msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." @@ -46520,7 +46523,7 @@ msgstr "Red #{0}: ID Sekvence mora biti {1} ili {2} za Operaciju {3}." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:528 msgid "Row #{0}: Serial No {1} cannot be returned since it was not transacted in original invoice {2}" -msgstr "" +msgstr "Red #{0}: Serijski Broj {1} ne može se vratiti jer nije naveden u originalnoj fakturi {2}" #: erpnext/stock/services/serial_batch_bundle_service.py:123 msgid "Row #{0}: Serial No {1} does not belong to Batch {2}" @@ -46637,11 +46640,11 @@ msgstr "Red #{0}: Šarža {1} je već istekla." #: erpnext/stock/doctype/stock_entry/stock_entry.py:408 msgid "Row #{0}: The job card item reference is missing. Kindly create the stock entry from the job card. If you have added the row manually then you won't be able to add job card item reference." -msgstr "" +msgstr "Red #{0}: Nedostaje referenca artikla na radnoj kartici. Stvori unos zaliha iz radne kartice. Ako ste red dodali ručno, nećete moći dodati referencu artikla na radnu karticu." #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:103 msgid "Row #{0}: The original Invoice {1} of return invoice {2} is not consolidated." -msgstr "" +msgstr "Red #{0}: Originalna Faktura {1} povratne fakture {2} nije konsolidovana." #: erpnext/stock/doctype/item/item.py:599 msgid "Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2}" @@ -46649,7 +46652,7 @@ msgstr "Red #{0}: Skladište {1} nije podređeno skladište grupnog skladišta { #: erpnext/manufacturing/doctype/workstation/workstation.py:190 msgid "Row #{0}: Timings conflict with row {1}" -msgstr "" +msgstr "Red #{0}: Vremenski sukob je u odnosu na red {1}" #: erpnext/assets/doctype/asset/asset.py:656 msgid "Row #{0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" @@ -46673,7 +46676,7 @@ msgstr "Red #{0}: Radni Nalog postoji za punu ili djelomičnu količinu artiikla #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:500 msgid "Row #{0}: You cannot add positive quantities in a return invoice. Please remove item {1} to complete the return." -msgstr "" +msgstr "Red #{0}: Ne možete dodati pozitivne količine u povratnu fakturu. Molimo vas da uklonite artikal {1} da biste dovršili povrat." #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:110 msgid "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." @@ -46685,7 +46688,7 @@ msgstr "Red #{0}: Odaberi Imovinu za Artikal {1}." #: erpnext/stock/doctype/pick_list/pick_list.py:235 msgid "Row #{0}: item {1} has been picked already." -msgstr "" +msgstr "Red #{0}: artikal {1} je već odabran." #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:142 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:207 @@ -46694,7 +46697,7 @@ msgstr "Red #{0}: {1}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:142 msgid "Row #{0}: {1} account is not of type {2}" -msgstr "" +msgstr "Red #{0}: {1} račun nije tipa {2}" #: erpnext/public/js/controllers/buying.js:261 msgid "Row #{0}: {1} can not be negative for item {2}" @@ -46714,11 +46717,11 @@ msgstr "Red #{0}: {1} od {2} bi trebao biti {3}. Ažuriraj {1} ili odaberi drugi #: erpnext/stock/doctype/item/item.py:1511 msgid "Row #{0}: {1} {2} does not belong to Company {3}. Please select valid {4}." -msgstr "" +msgstr "Red #{0}: {1} {2} ne pripada {3}. Odaberi važeći {4}." #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{0}: {1} {2} does not exist." -msgstr "" +msgstr "Red #{0}: {1} {2} ne postoji." #: erpnext/accounts/services/child_item_update.py:251 msgid "Row #{0}:Quantity for Item {1} cannot be zero." @@ -46895,7 +46898,7 @@ msgstr "Red {0}: Od vremena i do vremena je obavezano." #: erpnext/manufacturing/doctype/job_card/job_card.py:355 msgid "Row {0}: From Time and To Time of {1} are overlapping with {2}" -msgstr "" +msgstr "Red {0}: Vrijeme od i Vrijeme do {1} se preklapaju sa {2}" #: erpnext/projects/doctype/timesheet/timesheet.py:225 msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" @@ -46919,7 +46922,7 @@ msgstr "Red {0}: Nevažeća referenca {1}" #: erpnext/controllers/taxes_and_totals.py:134 msgid "Row {0}: Item Tax template for {1} updated as per validity and rate applied" -msgstr "" +msgstr "Red {0}: Predložak Pdv-a za Artikal {1} ažuriran je u skladu s važećim rokom i primijenjenom stopom" #: erpnext/controllers/selling_controller.py:659 msgid "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" @@ -46983,7 +46986,7 @@ msgstr "Red {0}: Odaberi Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:214 msgid "Row {0}: Please select a valid BOM for Item {1}." -msgstr "" +msgstr "Red {0}: Odaberi važeću Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:208 msgid "Row {0}: Please select an active BOM for Item {1}." @@ -47055,7 +47058,7 @@ msgstr "Red {0}: Cijeli iznos troška za račun {1} u {2} je već dodijeljen." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:103 msgid "Row {0}: The item {1}, quantity must be a positive number" -msgstr "" +msgstr "Red {0}: Artikal {1}, količina mora biti pozitivan broj" #: erpnext/accounts/services/taxes.py:269 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" @@ -47116,7 +47119,7 @@ msgstr "Red {0}: {1} {2} je povezan sa {3}. Odaberi dokument koji pripada {4}." #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:132 msgid "Row {0}: {1} {2} must be submitted" -msgstr "" +msgstr "Red {0}: {1} {2} mora biti podnešen" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:111 msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" @@ -47162,7 +47165,7 @@ msgstr "Redovi: {0} imaju 'Unos Plaćanja' kao Tip Reference. Ovo ne treba posta #: erpnext/controllers/accounts_controller.py:276 msgid "Rows: {0} in {1} section are invalid. Reference Name should point to a valid Payment Entry or Journal Entry." -msgstr "" +msgstr "Redovi: {0} u {1} sekciji su nevažeći. Naziv reference treba da ukazuje na važeći Unos Plaćanja ili Nalog Knjiženja." #. Label of the rule_applied (Check) field in DocType 'Pricing Rule Detail' #: erpnext/accounts/doctype/pricing_rule_detail/pricing_rule_detail.json @@ -47589,7 +47592,7 @@ msgstr "Prodajna Faktura nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:195 msgid "Sales Invoice isn't created by user {0}" -msgstr "" +msgstr "Prodajna Faktura nije kreirana od {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:472 msgid "Sales Invoice mode is activated in POS. Please create Sales Invoice instead." @@ -49055,7 +49058,7 @@ msgstr "Odabrani dokument mora biti u podnešenom stanju" #: erpnext/assets/doctype/asset/asset.py:1195 msgid "Selected {0} does not contain the Item Code {1}" -msgstr "" +msgstr "Odabrani {0} ne sadrži Šifru Artikla {1}" #. Option for the 'Pickup Type' (Select) field in DocType 'Shipment' #: erpnext/stock/doctype/shipment/shipment.json @@ -49393,7 +49396,7 @@ msgstr "Serijski broj je već dodijeljen" #: erpnext/assets/doctype/asset_repair/asset_repair.py:296 msgid "Serial No Bundle is mandatory for Item {0}" -msgstr "" +msgstr "Paket Serijskih Brojeva je obavezan za artikal {0}" #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:33 msgid "Serial No Count" @@ -49458,7 +49461,7 @@ msgstr "Serijski Broj i Šarža" #: erpnext/stock/doctype/stock_settings/stock_settings.js:93 msgid "Serial No and Batch Selector cannot be used when Use Serial / Batch Fields is enabled." -msgstr "" +msgstr "Serijski Broj i birač Šarže ne mogu se koristiti kada je omogućeno Koristi Serijski Broj / Šaržu." #. Name of a report #. Label of a Link in the Stock Workspace @@ -49501,7 +49504,7 @@ msgstr "Serijski Broj {0} ne postoji" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:378 msgid "Serial No {0} is already Delivered. You cannot use it again in Manufacture / Repack entry." -msgstr "" +msgstr "Serijski broj {0} je već dostavljen. Ne možete ga ponovno koristiti u unosu Proizvodnje / Ponovnog pakiranja." #: erpnext/public/js/utils/barcode_scanner.js:435 msgid "Serial No {0} is already added" @@ -49517,11 +49520,11 @@ msgstr "Serijski broj {0} nije u {1} {2}, i ne može se vratiti naspram {1} {2}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:343 msgid "Serial No {0} is under maintenance contract until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod ugovorom o održavanju do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:336 msgid "Serial No {0} is under warranty until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod garancijom do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:322 msgid "Serial No {0} not found" @@ -49657,7 +49660,7 @@ msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mi #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:298 msgid "Serial and Batch Bundle {0} should have voucher type as 'Maintenance Schedule'" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} treba imati tip verifikata kao 'Raspored Održavanja'" #. Label of the section_break_45 (Section Break) field in DocType #. 'Subcontracting Receipt Item' @@ -51136,7 +51139,7 @@ msgstr "Nedostaju neki obavezni podaci o poduzeću Nemate dozvolu da ih ažurira #: erpnext/www/book_appointment/index.js:248 msgid "Something went wrong, please try again" -msgstr "" +msgstr "Nešto nije u redu, pokušaj ponovo" #: erpnext/accounts/doctype/pricing_rule/utils.py:754 msgid "Sorry, this coupon code is no longer valid" @@ -51387,7 +51390,7 @@ msgstr "Raspodijeli proviziju među više prodavača." #: erpnext/buying/doctype/purchase_order/purchase_order.js:600 #: erpnext/public/js/controllers/buying.js:558 msgid "Splitting {0} units of {1}" -msgstr "" +msgstr "Dijeljenje {0} jedinica od {1}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2195 msgid "Splitting {0} {1} into {2} rows as per Payment Terms" @@ -51509,15 +51512,15 @@ msgstr "Poredak" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:73 msgid "Standing scores must be continuous and cover 0 to 100 without gaps or overlaps" -msgstr "" +msgstr "Aktualni rezultati moraju biti neprekidni i obuhvatiti raspon od 0 do 100 bez praznina ili preklapanja" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:76 msgid "Standing scores must cover the full range from 0 to 100" -msgstr "" +msgstr "Aktualni rezultati moraju pokrivati cijeli raspon od 0 do 100." #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:68 msgid "Standing {0} must have a minimum grade lower than its maximum grade" -msgstr "" +msgstr "{0} mora imati najmanje ocjene niže od svoje najviše ocjene" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:54 msgid "Start / Resume" @@ -51525,7 +51528,7 @@ msgstr "Pokreni / Nastavi" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:45 msgid "Start Date cannot be after End Date" -msgstr "" +msgstr "Datum početka ne može biti nakon datuma završetka" #: erpnext/crm/doctype/email_campaign/email_campaign.py:40 msgid "Start Date cannot be before the current date" @@ -51591,7 +51594,7 @@ msgstr "Započet je pozadinski zadatak za kreiranje {1} {0}. {2}" #: erpnext/public/js/bulk_transaction_processing.js:29 msgid "Starting a background job to create {0} {1}" -msgstr "" +msgstr "Započet je pozadinski zadatak za kreiranje {0} {1}" #. Label of the date_dist_from_left_edge (Float) field in DocType 'Cheque Print #. Template' @@ -51802,7 +51805,7 @@ msgstr "Unos Zaključanih Zaliha {0} već postoji za odabrani vremenski raspon" #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:100 msgid "Stock Closing Entry {0} has been queued for processing, the system will take some time to complete it." -msgstr "" +msgstr "Završni Unos Zaliha {0} je stavljen u red za obradu, sistemu će trebati neko vrijeme da ga završi." #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry_dashboard.py:9 msgid "Stock Closing Log" @@ -51885,11 +51888,11 @@ msgstr "Tip Unosa Zaliha" #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.py:65 msgid "Stock Entry Type {0} cannot be set as standard" -msgstr "" +msgstr "Tip Unosa Zaliha {0} ne može se postaviti kao standard" #: erpnext/stock/doctype/pick_list/mapper.py:289 msgid "Stock Entry has already been created against this Pick List" -msgstr "" +msgstr "Unos Zaliha je već kreiran naspram ove Liste Odabira" #: erpnext/stock/doctype/batch/batch.js:138 msgid "Stock Entry {0} created" @@ -51897,7 +51900,7 @@ msgstr "Unos Zaliha {0} je kreiran" #: erpnext/manufacturing/doctype/job_card/job_card.py:1639 msgid "Stock Entry {0} has been created" -msgstr "" +msgstr "Unos Zaliha {0} je stvoren" #: erpnext/accounts/doctype/journal_entry/journal_entry.py:997 msgid "Stock Entry {0} is not submitted" @@ -52510,7 +52513,7 @@ msgstr "Zaliha nije dostupna za Artikal {0} u Skladištu {1}." #: erpnext/selling/page/point_of_sale/pos_controller.js:826 msgid "Stock quantity is not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}." -msgstr "" +msgstr "Količina Zaliha nije dovoljna za Kod Artikla: {0} na skladištu {1}. Dostupna količina {2} {3}." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:255 msgid "Stock transactions before {0} are frozen" @@ -53980,7 +53983,7 @@ msgstr "Ciljna Imovina {0} ne pripada {1}" #: erpnext/assets/doctype/asset_capitalization/asset_capitalization.py:189 msgid "Target Asset {0} needs to be a composite asset" -msgstr "" +msgstr "Ciljana Imovina {0} mora biti objedinjena imovina" #. Name of a DocType #: erpnext/setup/doctype/target_detail/target_detail.json @@ -55090,7 +55093,7 @@ msgstr "Tekst prikazan u finansijskom izvještaju (npr. 'Ukupni Prihod', 'Gotovi #: erpnext/stock/doctype/packing_slip/packing_slip.py:89 msgid "The 'From Package No.' field must not be empty or have a value less than 1." -msgstr "" +msgstr "\"Od Paketa Broj.\" polje ne smije biti prazno niti njegova vrijednost manja od 1." #. Description of the 'Current BOM' (Link) field in DocType 'BOM Update Tool' #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.json @@ -55099,7 +55102,7 @@ msgstr "Sastavnica koja će biti zamijenjena" #: erpnext/controllers/subcontracting_controller.py:1056 msgid "The Batch No {0} has not been supplied against the {1} {2}" -msgstr "" +msgstr "Broj Šarže {0} nije dostavljen protiv {1} {2}" #: erpnext/stock/serial_batch_bundle.py:1557 msgid "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." @@ -55107,7 +55110,7 @@ msgstr "Šarža {0} ima negativnu količinu {1}. Da biste to riješili, idite na #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1590 msgid "The Batch {0} of item {1} has negative stock in the warehouse {2}{3}. Please add a stock quantity of {4} to proceed with this entry. If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or in the Stock Settings to proceed. However, enabling this setting may lead to negative stock in the system. So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate." -msgstr "" +msgstr "Šarža {0} artikla {1} ima negativne zalihe u skladištu {2}{3}. Dodaj količinu zaliha od {4} da biste nastavili s ovim unosom. Ako nije moguće izvršiti unos prilagođavanja, omogućite 'Dozvoli Negativne Zalihe za Šaržu' za Šaržu {0} ili u Postavkama Zaliha da biste nastavili. Međutim, omogućavanje ove postavke može dovesti do negativnih zaliha u sistemu. Stoga, molimo vas da osigurate da se nivoi zaliha što prije prilagode kako bi se održala ispravna stopa vrednovanja." #: erpnext/crm/doctype/email_campaign/email_campaign.py:71 msgid "The Campaign '{0}' already exists for the {1} '{2}'" @@ -55135,7 +55138,7 @@ msgstr "Knjigovodstveni Unosi će biti otkazani u pozadini, može potrajati neko #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1156 msgid "The Item {0} does not have Serial No or Batch No" -msgstr "" +msgstr "Artikal {0} nema Serijski niti Šaržni Broj" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:179 msgid "The Loyalty Program isn't valid for the selected company" @@ -55155,11 +55158,11 @@ msgstr "Lista Odabira koja ima Unose Rezervacije Zaliha ne može se ažurirati. #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:127 msgid "The Process Loss Qty has been reset as per the Job Card's Process Loss Qty" -msgstr "" +msgstr "Količinski Gubitak Procesa je poništen prema Radnim Karticama Količinskog Gubitka Procesa" #: erpnext/stock/doctype/stock_entry/stock_entry.py:1375 msgid "The Process Loss Qty has been reset as per the job card's Process Loss Qty" -msgstr "" +msgstr "Količinski Gubitak Procesa je poništen prema Radnim Karticama Količinskog Gubitka Procesa" #: erpnext/setup/doctype/sales_person/sales_person.py:102 msgid "The Sales Person is linked with {0}" @@ -55175,7 +55178,7 @@ msgstr "Serijski Broj {0} je rezervisan naspram {1} {2} i ne može se koristiti #: erpnext/controllers/subcontracting_controller.py:1071 msgid "The Serial Nos {0} have not been supplied against the {1} {2}" -msgstr "" +msgstr "Serijski Brojevi {0} nisu dostavljeni protiv {1} {2}" #: erpnext/stock/doctype/stock_entry/stock_entry.py:950 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" @@ -55231,7 +55234,7 @@ msgstr "Završena količina {0} operacije {1} ne može biti veća od završene k #: erpnext/accounts/doctype/dunning/dunning.py:87 msgid "The currency of invoice {0} ({1}) is different from the currency of this dunning ({2})." -msgstr "" +msgstr "Valuta Fakture {0} ({1}) se razlikuje od valute ove Opomene ({2})." #: erpnext/selling/page/point_of_sale/pos_controller.js:200 msgid "The current POS opening entry is outdated. Please close it and create a new one." @@ -55284,7 +55287,7 @@ msgstr "Polje {0} u redu {1} nije postavljeno" #: erpnext/stock/stock_ledger.py:369 msgid "The field {0} is required for reposting" -msgstr "" +msgstr "Polje {0} je obavezno za ponovno knjiženje" #: erpnext/accounts/doctype/share_transfer/share_transfer.py:188 msgid "The fields From Shareholder and To Shareholder cannot be blank" @@ -55309,7 +55312,7 @@ msgstr "Brojevi Folija nisu usklađeni" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:306 msgid "The following Items, having Putaway Rules, could not be accommodated:" -msgstr "" +msgstr "Sljedeći artikli, koji imaju Pravila Odlaganja na Stranu, nisu mogli biti primjenjene:" #: erpnext/assets/doctype/asset_repair/asset_repair.py:137 msgid "The following Purchase Invoices are not submitted:" @@ -55337,7 +55340,7 @@ msgstr "Sljedeći personal još uvijek podnose izvještaj {0}:" #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py:185 msgid "The following invalid Pricing Rules are deleted:{0}" -msgstr "" +msgstr "Sljedeća nevažeća Pravila Cijena se brišu:{0}" #: erpnext/accounts/doctype/payment_request/payment_request.py:782 msgid "The following payment schedule(s) already exist:\n" @@ -55386,7 +55389,7 @@ msgstr "Artikli {items} nisu označeni kao {type_of} artikli. Možete ih omoguć #: erpnext/manufacturing/doctype/workstation/workstation.py:595 msgid "The job card {0} is in {1} state and you cannot complete it." -msgstr "" +msgstr "Radna Kartica {0} je u {1} stanju i ne možete je završiti." #: erpnext/manufacturing/doctype/workstation/workstation.py:589 msgid "The job card {0} is in {1} state and you cannot start it again." @@ -55424,11 +55427,11 @@ msgstr "Početno stanje se možda nije usklađeno s vašim bankovnim izvodom. Ž #: erpnext/manufacturing/doctype/operation/operation.py:43 msgid "The operation {0} cannot be added multiple times" -msgstr "" +msgstr "Operacija {0} se ne može dodati više puta" #: erpnext/manufacturing/doctype/operation/operation.py:48 msgid "The operation {0} cannot be its own sub-operation" -msgstr "" +msgstr "Operacija {0} ne može biti vlastita podoperacija" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:107 msgid "The original invoice should be consolidated before or along with the return invoice." @@ -55478,7 +55481,7 @@ msgstr "Procenat kojim vam je dozvoljeno prenijeti više naspram naručene koli #: erpnext/stock/doctype/item_price/item_price.py:71 msgid "The price list {0} does not exist or is disabled" -msgstr "" +msgstr "Cjenovnik {0} ne postoji ili je onemogućen" #. Description of the 'Last Purchase Rate' (Float) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -55507,7 +55510,7 @@ msgstr "Odabrane Sastavnice nisu za istu artikal" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:544 msgid "The selected change account {0} does not belong to Company {1}." -msgstr "" +msgstr "Odabrani račun povrata {0} ne pripada {1}." #: erpnext/stock/doctype/batch/batch.py:157 msgid "The selected item cannot have Batch" @@ -55524,7 +55527,7 @@ msgstr "Prodavač i Kupac ne mogu biti isti" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:186 #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:198 msgid "The serial and batch bundle {0} is not linked to {1} {2}" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} nije povezan sa {1} {2}" #: erpnext/stock/doctype/batch/batch.py:386 msgid "The serial no {0} does not belong to item {1}" @@ -55578,7 +55581,7 @@ msgstr "Zadatak je stavljen u red kao pozadinski posao. U slučaju da postoji bi #: erpnext/stock/doctype/material_request/material_request.py:352 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}" -msgstr "" +msgstr "Ukupna količina izdavanja / prijenosa {0} u Materijalnom Nalogu {1} ne može biti veća od dozvoljene tražene količine {2} za artikal {3}" #: erpnext/stock/doctype/material_request/material_request.py:359 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" @@ -55658,7 +55661,7 @@ msgstr "{0} {1} se ne poklapa s {0} {2} u {3} {4}" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1730 msgid "The {0} {1} is in submitted state, please cancel it first" -msgstr "" +msgstr "{0} {1} je u podnešenom stanju, prvo ga otkažite" #: erpnext/manufacturing/doctype/job_card/job_card.py:1075 msgid "The {0} {1} is used to calculate the valuation cost for the finished good {2}." @@ -55699,7 +55702,7 @@ msgstr "U sistemu nema unosa kod kojih je datum odobravanja prije datuma knjiže #: erpnext/stock/report/item_variant_details/item_variant_details.py:25 msgid "There are no item variants for the selected item" -msgstr "" +msgstr "Nema artikal varijanti za odabrani artikal" #: erpnext/www/book_appointment/index.js:95 msgid "There are no slots available on this date" @@ -55747,7 +55750,7 @@ msgstr "Postoji jedna neusklađena transakcija prije {0}." #: erpnext/stock/doctype/stock_entry/stock_entry.py:887 msgid "There must be at least 1 Finished Good in this Stock Entry" -msgstr "" +msgstr "U ovom Unosu Zaliha mora biti najmanje jedan gotov proizvod" #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:152 msgid "There was an error creating Bank Account while linking with Plaid." @@ -55759,7 +55762,7 @@ msgstr "Došlo je do greške pri sinhronizaciji transakcija." #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:174 msgid "There was an error updating Bank Account {0} while linking with Plaid." -msgstr "" +msgstr "Došlo je do greške prilikom ažuriranja Bankovnog Računa {0} prilikom povezivanja s Plaid-om." #: banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx:81 msgid "There was an error while importing the bank statement." @@ -55815,7 +55818,7 @@ msgstr "Ovaj Unos Plaćanja je usklađen sa {0}. Otkazivanjem će se automatski #: erpnext/selling/doctype/product_bundle/product_bundle.py:121 msgid "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" -msgstr "" +msgstr "Ovaj Artikal Paket je povezan sa {0}. Morat ćete otkazati ove dokumente kako biste izbrisali ovaj Artikal Paket" #: erpnext/buying/doctype/purchase_order/mapper.py:251 msgid "This Purchase Order has been fully subcontracted." @@ -56156,7 +56159,7 @@ msgstr "Ovo će ograničiti pristup korisnika drugim zapisima zaposlenih" #: erpnext/controllers/selling_controller.py:901 msgid "This {0} will be treated as material transfer." -msgstr "" +msgstr "Ovaj {0} će se tretirati kao prijenos materijala." #. Option for the 'Under Withheld Reason' (Select) field in DocType 'Tax #. Withholding Entry' @@ -56288,7 +56291,7 @@ msgstr "Vremenska Linija" #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher" -msgstr "" +msgstr "Vremensko ograničenje (u sekundama) za svaki pozadinski zadatak koji je stavljen u red čekanja od strane verifikata za zatvaranje knjgovodstvenog perioda" #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:36 #: erpnext/public/js/projects/timer.js:5 @@ -56577,7 +56580,7 @@ msgstr "Do Vremena" #: erpnext/projects/doctype/timesheet_detail/timesheet_detail.py:108 msgid "To Time cannot be before From Time" -msgstr "" +msgstr "Do Vremena ne može biti prije Od Vremena" #. Description of the 'Referral Code' (Data) field in DocType 'Sales Partner' #: erpnext/setup/doctype/sales_partner/sales_partner.json @@ -56633,7 +56636,7 @@ msgstr "Dostava Klijentu" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:276 msgid "To cancel a {0} you need to cancel the POS Closing Entry {1}." -msgstr "" +msgstr "Da biste otkazali {0}, potrebno je da otkažete unos zatvaranja Kase {1}." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:290 msgid "To cancel this Sales Invoice you need to cancel the POS Closing Entry {0}." @@ -56645,7 +56648,7 @@ msgstr "Za kreiranje Zahtjeva Plaćanja obavezan je referentni dokument" #: erpnext/assets/doctype/asset_category/asset_category.py:120 msgid "To enable Capital Work in Progress Accounting, you must select Capital Work in Progress Account in accounts table" -msgstr "" +msgstr "Da biste omogućili knjigovodstvo nedovršenih kapitalnih radova, morate odabrati Račun nedovršenih kapitalnih radova u tabeli računa" #: erpnext/manufacturing/doctype/production_plan/production_plan.js:734 msgid "To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked." @@ -56824,19 +56827,19 @@ msgstr "Ukupni Predujam" #: erpnext/public/js/utils.js:250 msgid "Total Advance Paid" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed" #: erpnext/public/js/utils.js:195 msgid "Total Advance Paid: {0}" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed: {0}" #: erpnext/public/js/utils.js:252 msgid "Total Advance Received" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed" #: erpnext/public/js/utils.js:198 msgid "Total Advance Received: {0}" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed: {0}" #. Label of the total_allocated_amount (Currency) field in DocType 'Payment #. Entry' @@ -57495,7 +57498,7 @@ msgstr "Ukupno Vrijeme u minutama" #: erpnext/public/js/utils.js:253 msgid "Total Unpaid" -msgstr "" +msgstr "Ukupno Neplaćeno" #: erpnext/public/js/utils.js:200 msgid "Total Unpaid: {0}" @@ -57595,7 +57598,7 @@ msgstr "Ukupno sati: {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:574 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:190 msgid "Total payments amount can't be greater than {0}" -msgstr "" +msgstr "Ukupan iznos plaćanja ne može biti veći od {0}" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:66 msgid "Total percentage against cost centers should be 100" @@ -57614,7 +57617,7 @@ msgstr "Ukupno {0} ({1})" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:248 msgid "Total {0} for all items is zero, maybe you should change 'Distribute Charges Based On'" -msgstr "" +msgstr "Ukupni iznos {0} a za sve artikle je nula, možda biste trebali promijeniti 'Raspodjeli Troškove na Osnovu'" #: erpnext/controllers/trends.py:25 erpnext/controllers/trends.py:32 msgid "Total(Amt)" @@ -58155,7 +58158,7 @@ msgstr "Probni Bilans Stranke" #: erpnext/accounts/report/trial_balance/trial_balance.py:595 msgid "Trial Balance requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Probni Bilans zahtijeva sinhronizaciju {0} sa DuckDB-om" #. Label of the trial_period_end (Date) field in DocType 'Subscription' #: erpnext/accounts/doctype/subscription/subscription.json @@ -59399,7 +59402,7 @@ msgstr "Korisnik nije primijenio pravilo na fakturi {0}" #: erpnext/crm/frappe_crm_api.py:175 msgid "User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Korisniku nije dozvoljeno sinhroniziranje podataka iz Prodajne Podrške u Sistem. Kontaktiraj Odgovornog Sistema." #: erpnext/setup/doctype/employee/employee.py:298 msgid "User {0} does not exist" @@ -59415,7 +59418,7 @@ msgstr "Korisnik {0} je već dodijeljen {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:62 msgid "User {0} is disabled. Please select valid user/cashier" -msgstr "" +msgstr "Korisnik {0} je onemogućen. Odaberi važećeg korisnika/blagajnika" #: erpnext/setup/doctype/employee/employee.py:365 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." @@ -59763,7 +59766,7 @@ msgstr "Naknade za tip vrijednovanja ne mogu biti označene kao Inkluzivne" #: erpnext/public/js/controllers/accounts.js:228 msgid "Valuation type charges cannot be marked as Inclusive" -msgstr "" +msgstr "Naknade tipa procjene vrijednosti ne mogu biti označene kao uključene." #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:58 msgid "Value (G - D)" @@ -61329,11 +61332,11 @@ msgstr "Sažetka Izvještaja Radnog Naloga" #: erpnext/stock/doctype/material_request/material_request.py:579 msgid "Work Order cannot be created for the following reason:
{0}" -msgstr "" +msgstr "Radni Nalog se ne može kreirati iz sljedećeg razloga:
{0}" #: erpnext/manufacturing/doctype/work_order/work_order.py:857 msgid "Work Order cannot be raised against an Item Template" -msgstr "" +msgstr "Radni Nalog se nemože pokrenuti naspram Šablona Artikla" #: erpnext/manufacturing/doctype/work_order/work_order.py:1123 #: erpnext/manufacturing/doctype/work_order/work_order.py:1170 @@ -61686,7 +61689,7 @@ msgstr "Uvoziš podatke za Listu Koda:" #: erpnext/accounts/services/child_item_update.py:232 msgid "You are not allowed to update as per the conditions set in {0} Workflow." -msgstr "" +msgstr "Nije vam dozvoljeno ažuriranje prema uslovima postavljenim u {0} Radnom Toku." #: erpnext/accounts/services/gl_validator.py:114 msgid "You are not authorized to add or update entries before {0}" @@ -61706,7 +61709,7 @@ msgstr "Birate više od potrebne količine za artikal {0}. Provjerite postoji li #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:111 msgid "You can add the original invoice {0} manually to proceed." -msgstr "" +msgstr "Možete ručno dodati originalnu fakturu {0} da biste nastavili." #: banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx:743 msgid "You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25)." @@ -61718,7 +61721,7 @@ msgstr "Takođe možete kopirati i zalijepiti ovu vezu u svoj pretraživač" #: erpnext/assets/doctype/asset_category/asset_category.py:124 msgid "You can also set default CWIP account in Company {0}" -msgstr "" +msgstr "Također možete postaviti standard Račun Kapitalnog Posla u Toku za {0}" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:761 msgid "You can change the parent account to a Balance Sheet account or select a different account." @@ -61747,7 +61750,7 @@ msgstr "Možete odabrati samo jedan način plaćanja kao standard" #: erpnext/selling/page/point_of_sale/pos_payment.js:595 msgid "You can redeem up to {0}." -msgstr "" +msgstr "Možete iskoristiti do {0}." #: banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx:193 msgid "You can reset the clearing dates of these entries here." @@ -61779,11 +61782,11 @@ msgstr "Ne možete kreirati {0} unutar zatvorenog Knjigovodstvenog Perioda {1}" #: erpnext/accounts/services/gl_validator.py:64 msgid "You cannot create or cancel any accounting entries within the closed Accounting Period {0}" -msgstr "" +msgstr "Ne možete izraditi niti otkazati nikakve knjigovodstvene zapise unutar zatvorenog knjigovodstvenog perioda. {0}" #: erpnext/accounts/services/gl_validator.py:145 msgid "You cannot create/amend any accounting entries until this date." -msgstr "" +msgstr "Ne možete izraditi/izmijeniti bilo koje knjigovodstvene unose do ovog datuma." #: erpnext/accounts/doctype/journal_entry/journal_entry.py:675 msgid "You cannot credit and debit same account at the same time" @@ -61795,7 +61798,7 @@ msgstr "Ne možete izbrisati tip projekta 'Eksterni'" #: erpnext/setup/doctype/department/department.js:19 msgid "You cannot edit the root node." -msgstr "" +msgstr "Ne možete uređivati korijenski čvor." #: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:198 msgid "You cannot enable both the settings '{0}' and '{1}'." @@ -61803,15 +61806,15 @@ msgstr "Ne možete omogućiti i '{0}' i '{1} postavke." #: erpnext/manufacturing/doctype/job_card/job_card.py:1441 msgid "You cannot make any changes to Job Card since Work Order is closed." -msgstr "" +msgstr "Ne možete napraviti nikakve promjene na Radnoj Kartici jer je Radni Nalog zatvoren." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:167 msgid "You cannot outward the following {0} as they are either Delivered, Inactive or located in a different warehouse." -msgstr "" +msgstr "Ne možete poslati sljedeće {0} jer su ili Dostavljeni, Neaktivni ili se nalaze u drugom skladištu." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:229 msgid "You cannot process the serial number {0} as it has already been used in the SABB {1}. {2} If you want to inward the same serial number multiple times, then enable 'Allow existing Serial No to be Manufactured/Received again' in the {3}" -msgstr "" +msgstr "Ne možete obraditi serijski broj {0} jer je već korišten u Serijskom i Šaržnom Paketu {1}. {2} ako želite da primite isti serijski broj više puta, tada omogući 'Dozvoli da se postojeći Serijski Broj ponovo Proizvede/Primi' u {3}" #: erpnext/selling/page/point_of_sale/pos_payment.js:625 msgid "You cannot redeem more than {0}." @@ -61819,7 +61822,7 @@ msgstr "Ne možete iskoristiti više od {0}." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:212 msgid "You cannot repost item valuation before {0}" -msgstr "" +msgstr "Ne možete ponovo knjižiti procjenu vrijednosti artikla prije {0}" #: erpnext/accounts/doctype/subscription/subscription.py:832 msgid "You cannot restart a Subscription that is not cancelled." @@ -61827,7 +61830,7 @@ msgstr "Ne možete ponovo pokrenuti Pretplatu koja nije otkazana." #: erpnext/selling/page/point_of_sale/pos_payment.js:281 msgid "You cannot submit an empty order." -msgstr "" +msgstr "Ne možete podnijeti prazan nalog." #: erpnext/selling/page/point_of_sale/pos_payment.js:280 msgid "You cannot submit the order without payment." @@ -61843,7 +61846,7 @@ msgstr "Ne možete {0} ovaj dokument jer postoji drugi Unos Zatvaranje Perioda { #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:165 msgid "You do not have enough permission to access {0}: {1}" -msgstr "" +msgstr "Nemate dovoljno dozvola za pristup {0}: {1}" #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:82 msgid "You do not have permission to import and submit bank transactions" @@ -61856,7 +61859,7 @@ msgstr "Nemate dozvolu za uvoz bankovnih transakcija" #: erpnext/accounts/services/child_item_update.py:210 msgid "You do not have permissions to {0} items in a {1}." -msgstr "" +msgstr "Nemate dozvole za {0} artikla u {1}." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:187 msgid "You don't have enough Loyalty Points to redeem" @@ -61884,7 +61887,7 @@ msgstr "Nemate dozvolu za ažuriranje ovog dokumenta.Kontaktiraj Odgovornog Sist #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:310 msgid "You had {0} errors while creating opening invoices. Check {1} for more details" -msgstr "" +msgstr "Imali ste {0} grešaka prilikom izrade početnih faktura. Pogledaj {1} za više detalja" #: erpnext/public/js/utils.js:1055 msgid "You have already selected items from {0} {1}" @@ -61904,7 +61907,7 @@ msgstr "Omogućili ste {0} i {1} u {2}. Ovo može dovesti do umetanja cijena iz #: erpnext/stock/doctype/shipment/shipment.js:442 msgid "You have entered a duplicate Delivery Note on row {0}. Please rectify and try again." -msgstr "" +msgstr "Unijeli ste duplikat Dostavnice u red {0}. Ispravi grešku i pokušaj ponovo." #: banking/src/components/features/BankReconciliation/BankPicker.tsx:64 msgid "You have not added any bank accounts to your company." @@ -61928,7 +61931,7 @@ msgstr "Morate odabrati Klijenta prije dodavanja Artikla." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:282 msgid "You need to cancel POS Closing Entry {0} to be able to cancel this document." -msgstr "" +msgstr "Morate otkazati Unos Zatvaranje Kase {0} da biste mogli otkazati ovaj dokument." #: erpnext/accounts/services/taxes.py:277 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." @@ -61984,7 +61987,7 @@ msgstr "Nulto Stanje" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353 msgid "Zero Balance Journal: {0}" -msgstr "" +msgstr "Žurnal Nultog Stanja: {0}" #: erpnext/regional/report/uae_vat_201/uae_vat_201.py:78 msgid "Zero Rated" @@ -62108,7 +62111,7 @@ msgstr "naziv polja" #: erpnext/setup/doctype/item_group/item_group.py:49 msgid "for tax category {0}" -msgstr "" +msgstr "za PDV kategoriju {0}" #. Option for the 'Service Provider' (Select) field in DocType 'Currency #. Exchange Settings' @@ -62467,7 +62470,7 @@ msgstr "{0} ne može biti negativan" #: erpnext/accounts/doctype/sales_invoice/services/loyalty.py:77 msgid "{0} cannot be cancelled since the Loyalty Points earned has been redeemed. First cancel the {1} No {2}" -msgstr "" +msgstr "{0} se ne može otkazati jer su zarađeni bodovi lojalnosti iskorišteni. Prvo otkaži {1} Broj {2}" #: erpnext/accounts/doctype/pos_settings/pos_settings.py:53 msgid "{0} cannot be changed with opened Opening Entries." @@ -62475,7 +62478,7 @@ msgstr "{0} se ne može mijenjati s otvorenim Početnim Unosima." #: erpnext/public/js/utils/sales_common.js:336 msgid "{0} cannot be greater than 100" -msgstr "" +msgstr "{0} ne može biti veće od 100" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:136 msgid "{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}" @@ -62544,7 +62547,7 @@ msgstr "{0} je uspješno podnešen" #: erpnext/controllers/buying_controller.py:289 msgid "{0} has submitted assets linked to it. You need to cancel the assets to create purchase return." -msgstr "" +msgstr "{0} je podnio/la imovinu povezanu s njim/njom. Morate otkazati imovinu da biste izradili povrat." #: erpnext/projects/doctype/project/project_dashboard.html:15 msgid "{0} hours" @@ -62556,7 +62559,7 @@ msgstr "{0} u redu {1}" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:66 msgid "{0} is a child company." -msgstr "" +msgstr "{0} je podređeno poduzeće." #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:465 msgid "{0} is a child table and will be deleted automatically with its parent" @@ -62639,7 +62642,7 @@ msgstr "{0} nije omogućen u {1}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:649 msgid "{0} is not running. Cannot trigger events for this document" -msgstr "" +msgstr "{0} se ne izvršava. Nije moguće pokrenuti događaje za ovaj dokument" #: erpnext/stock/doctype/material_request/material_request.py:478 msgid "{0} is not the default supplier for any items." @@ -62647,7 +62650,7 @@ msgstr "{0} nije standard dobavljač za bilo koji artikal." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2686 msgid "{0} is on hold until {1}" -msgstr "" +msgstr "{0} je na čekanju do {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:68 msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry." @@ -62822,11 +62825,11 @@ msgstr "{0} {1} je već povezan sa Zajedničkim Kodom {2}." #: erpnext/accounts/doctype/party_link/party_link.py:53 #: erpnext/accounts/doctype/party_link/party_link.py:63 msgid "{0} {1} is already linked with another {2}" -msgstr "" +msgstr "{0} {1} je već povezan sa drugim {2}" #: erpnext/accounts/doctype/party_link/party_link.py:40 msgid "{0} {1} is already linked with {2} {3}" -msgstr "" +msgstr "{0} {1} je već povezan s {2} {3}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:711 msgid "{0} {1} is associated with {2}, but Party Account is {3}" @@ -62867,7 +62870,7 @@ msgstr "{0} {1} nije aktivan" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:452 msgid "{0} {1} is not affecting bank account {2}" -msgstr "" +msgstr "{0} {1} ne utiče na bankovni račun {2}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:688 msgid "{0} {1} is not associated with {2} {3}" diff --git a/erpnext/locale/fa.po b/erpnext/locale/fa.po index f8f26879689..326be58cd19 100644 --- a/erpnext/locale/fa.po +++ b/erpnext/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-29 20:08\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -14218,7 +14218,7 @@ msgstr "آدرس فعلی" #. Label of the current_accommodation_type (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Current Address Is" -msgstr "آدرس فعلی است" +msgstr "آدرس فعلی" #. Label of the current_amount (Currency) field in DocType 'Stock #. Reconciliation Item' @@ -17767,7 +17767,7 @@ msgstr "سود سهام پرداخت شده" #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Divorced" -msgstr "جدا شده" +msgstr "طلاق گرفته" #. Option for the 'Status' (Select) field in DocType 'Lead' #: erpnext/crm/doctype/lead/lead.json @@ -30241,7 +30241,7 @@ msgstr "متخصص بازاریابی" #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Married" -msgstr "متاهل" +msgstr "متأهل" #: erpnext/setup/setup_wizard/data/marketing_source.txt:7 msgid "Mass Mailing" @@ -34688,7 +34688,7 @@ msgstr "" #. Option for the 'Current Address Is' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Owned" -msgstr "مالکیت" +msgstr "ملکی" #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.js:29 #: erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py:24 @@ -37162,7 +37162,7 @@ msgstr "آدرس دائمی" #. 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Permanent Address Is" -msgstr "آدرس دائمی است" +msgstr "آدرس دائمی" #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:73 #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:77 @@ -44208,7 +44208,7 @@ msgstr "اجاره" #. Option for the 'Current Address Is' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Rented" -msgstr "اجاره شده" +msgstr "استیجاری" #. Label of the reorder_level (Float) field in DocType 'Material Request Item' #: erpnext/stock/doctype/material_request_item/material_request_item.json @@ -50940,7 +50940,7 @@ msgstr "" #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Single" -msgstr "تنها" +msgstr "مجرد" #. Option for the 'Bank Entry Type' (Select) field in DocType 'Bank Transaction #. Rule' @@ -60982,7 +60982,7 @@ msgstr "هنگام تهیه فاکتور خرید از سفارش خرید، ب #. Option for the 'Marital Status' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json msgid "Widowed" -msgstr "بیوه" +msgstr "همسر فوت شده" #. Label of the width (Float) field in DocType 'Shipment Parcel' #. Label of the width (Float) field in DocType 'Shipment Parcel Template' @@ -61884,7 +61884,7 @@ msgstr "تراز صفر" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353 msgid "Zero Balance Journal: {0}" -msgstr "" +msgstr "دفتر تراز صفر: {0}" #: erpnext/regional/report/uae_vat_201/uae_vat_201.py:78 msgid "Zero Rated" @@ -62008,7 +62008,7 @@ msgstr "fieldname" #: erpnext/setup/doctype/item_group/item_group.py:49 msgid "for tax category {0}" -msgstr "" +msgstr "برای دسته بندی مالیاتی {0}" #. Option for the 'Service Provider' (Select) field in DocType 'Currency #. Exchange Settings' @@ -62375,7 +62375,7 @@ msgstr "" #: erpnext/public/js/utils/sales_common.js:336 msgid "{0} cannot be greater than 100" -msgstr "" +msgstr "{0} نمی‌تواند بزرگتر از ۱۰۰ باشد" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:136 msgid "{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}" @@ -62444,7 +62444,7 @@ msgstr "{0} با موفقیت ارسال شد" #: erpnext/controllers/buying_controller.py:289 msgid "{0} has submitted assets linked to it. You need to cancel the assets to create purchase return." -msgstr "" +msgstr "{0} دارایی‌های مرتبط با آن را ارسال کرده است. برای ایجاد بازگشت خرید، باید دارایی‌ها را لغو کنید." #: erpnext/projects/doctype/project/project_dashboard.html:15 msgid "{0} hours" @@ -62456,7 +62456,7 @@ msgstr "{0} در ردیف {1}" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:66 msgid "{0} is a child company." -msgstr "" +msgstr "{0} یک شرکت فرزند است." #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:465 msgid "{0} is a child table and will be deleted automatically with its parent" @@ -62539,7 +62539,7 @@ msgstr "{0} در {1} فعال نیست" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:649 msgid "{0} is not running. Cannot trigger events for this document" -msgstr "" +msgstr "{0} در حال اجرا نیست. نمی‌توان رویدادها را برای این سند فعال کرد" #: erpnext/stock/doctype/material_request/material_request.py:478 msgid "{0} is not the default supplier for any items." @@ -62547,7 +62547,7 @@ msgstr "{0} تامین کننده پیش‌فرض هیچ موردی نیست." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2686 msgid "{0} is on hold until {1}" -msgstr "" +msgstr "{0} تا زمان {1} در حالت انتظار است" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:68 msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry." @@ -62722,11 +62722,11 @@ msgstr "" #: erpnext/accounts/doctype/party_link/party_link.py:53 #: erpnext/accounts/doctype/party_link/party_link.py:63 msgid "{0} {1} is already linked with another {2}" -msgstr "" +msgstr "{0} {1} از قبل به {2} دیگری لینک شده است" #: erpnext/accounts/doctype/party_link/party_link.py:40 msgid "{0} {1} is already linked with {2} {3}" -msgstr "" +msgstr "{0} {1} از قبل به {2} {3} لینک شده است" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:711 msgid "{0} {1} is associated with {2}, but Party Account is {3}" @@ -62767,7 +62767,7 @@ msgstr "{0} {1} فعال نیست" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:452 msgid "{0} {1} is not affecting bank account {2}" -msgstr "" +msgstr "{0} {1} تاثیری بر حساب بانکی {2} ندارد" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:688 msgid "{0} {1} is not associated with {2} {3}" diff --git a/erpnext/locale/hr.po b/erpnext/locale/hr.po index 71ebf3f6525..221c1d12322 100644 --- a/erpnext/locale/hr.po +++ b/erpnext/locale/hr.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-28 20:03\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Croatian\n" "MIME-Version: 1.0\n" @@ -269,7 +269,7 @@ msgstr "'Dozvoli višestruke Prodajne Naloge naspram Nabavnog Naloga Klijenta'" #: erpnext/controllers/trends.py:62 msgid "'Based On' and 'Group By' can not be the same" -msgstr "" +msgstr "'Na Temelju' i 'Grupiraj Po' ne mogu biti isti" #: erpnext/selling/report/inactive_customers/inactive_customers.py:23 msgid "'Days Since Last Order' must be greater than or equal to zero" @@ -295,15 +295,15 @@ msgstr "'Od datuma' mora biti nakon 'Do datuma'" #: erpnext/stock/doctype/item/item.py:466 msgid "'Has Serial No' cannot be 'Yes' for non-stock item" -msgstr "" +msgstr "'Ima Serijski Broj' ne može biti 'Da' za artikal koji nije na zalihama" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:145 msgid "'Inspection Required before Delivery' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Dostave' je onemogućena za artikal {0}, nema potrebe za izradom Kontrole Kvaliteta" #: erpnext/stock/doctype/quality_inspection/quality_inspection.py:136 msgid "'Inspection Required before Purchase' is disabled for the item {0}, no need to create the QI" -msgstr "" +msgstr "'Kontrola Obavezna prije Nabave' je onemogućena za artikal {0}, nema potrebe za izradom Kontrole Kvaliteta" #: erpnext/stock/report/stock_ledger/stock_ledger.py:684 #: erpnext/stock/report/stock_ledger/stock_ledger.py:725 @@ -323,7 +323,7 @@ msgstr "'Do Paketa Broj' ne može biti manje od 'Od Paketa Broj.'" #: erpnext/controllers/sales_and_purchase_return.py:80 msgid "'Update Stock' cannot be checked because items are not delivered via {0}" -msgstr "" +msgstr "'Ažuriraj Zalihe' se ne može provjeriti jer se artikli ne dostavljaju putem {0}" #: erpnext/accounts/doctype/sales_invoice/services/fixed_assets.py:46 msgid "'Update Stock' cannot be checked for fixed asset sale" @@ -827,7 +827,7 @@ msgstr "

Ne možese fakturisati više od predviđenog iznosa za sljedeće arti #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py:159 msgid "

Following {0}s do not belong to Company {1}:

" -msgstr "" +msgstr "

Slijedeći {0} ne pripadaju {1} :

" #. Content of the 'html_llwp' (HTML) field in DocType 'Request for Quotation' #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -1042,7 +1042,7 @@ msgstr "A - C" #: erpnext/selling/doctype/customer/customer.py:358 msgid "A Customer Group exists with the same name. Please change the Customer name or rename the Customer Group" -msgstr "" +msgstr "Grupa Klijenta postoji sa istim imenom, molimo promijenite naziv klijenta ili preimenujte Grupu Klijenta" #: erpnext/manufacturing/doctype/workstation/workstation.js:73 msgid "A Holiday List can be added to exclude counting these days for the Workstation." @@ -1054,7 +1054,7 @@ msgstr "Potencijalni Klijent zahtijeva ili ime osobe ili ime tvrtke" #: erpnext/stock/doctype/packing_slip/packing_slip.py:83 msgid "A Packing Slip can only be created for a Draft Delivery Note." -msgstr "" +msgstr "Otpremnica se može kreirati samo za nacrt Dostavnice." #: erpnext/accounts/services/gl_validator.py:123 msgid "A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more." @@ -1312,7 +1312,7 @@ msgstr "Pristupni ključ je potreban za davaoca usluga: {0}" #: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:423 msgid "Access to Request for Quotation from the portal is disabled. To allow access, enable it in Portal Settings." -msgstr "" +msgstr "Pristup zahtjevu za ponudu sa portala je onemogućen. Da biste omogućili pristup, omogućite ga u Postavkama Portala." #. Description of the 'Common Code' (Data) field in DocType 'UOM' #: erpnext/setup/doctype/uom/uom.json @@ -2062,7 +2062,7 @@ msgstr "Knjigovodstveni Period" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:49 msgid "Accounting Period cannot be created for a future date. End Date {0} is after today." -msgstr "" +msgstr "Knjigovodstveno razdoblje ne može se izraditi za budući datum. Datum završetka {0} je sutra." #: erpnext/accounts/doctype/accounting_period/accounting_period.py:77 msgid "Accounting Period overlaps with {0}" @@ -2921,7 +2921,7 @@ msgstr "Dodata uloga dobavljača korisniku {0}." #: erpnext/controllers/website_list_for_contact.py:311 msgid "Added {1} role to user {0}." -msgstr "" +msgstr "Dodana je uloga {1} korisniku {0}." #: erpnext/crm/doctype/lead/lead.js:81 msgid "Adding Lead to Prospect..." @@ -3183,7 +3183,7 @@ msgstr "Dodatna Prenesena Količina" #: erpnext/manufacturing/doctype/work_order/work_order.py:591 msgid "Additional Transferred Qty {0} cannot be greater than {1}. To fix this, increase the percentage value of the field 'Transfer Extra Raw Materials to WIP' in Manufacturing Settings." -msgstr "" +msgstr "Dodatna Prenesena Količina {0} ne može biti veća od {1}. Da biste ovo ispravili, povećajte postotnu vrijednostpolja 'Prenesi Dodatne Sirovine u Nedovršenu Proizvodnju'u Postavkama Proizvodnje." #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:630 msgid "Additional {0} {1} of item {2} required as per BOM to complete this transaction" @@ -3779,7 +3779,7 @@ msgstr "Algoritam" #: erpnext/buying/doctype/supplier/supplier.json #: erpnext/selling/doctype/customer/customer.json msgid "Alias" -msgstr "" +msgstr "Nadimak" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:165 #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:185 @@ -3989,7 +3989,7 @@ msgstr "Svi komentari i e-pošta kopirat će se iz jednog dokumenta u drugi novo #: erpnext/selling/page/point_of_sale/pos_past_order_summary.js:204 msgid "All the items have already been returned." -msgstr "" +msgstr "Svi artikli su već vraćeni." #: erpnext/manufacturing/doctype/work_order/work_order.js:1272 msgid "All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table." @@ -3997,7 +3997,7 @@ msgstr "Svi obavezni Artikli (sirovine) bit će preuzeti iz Sastavnice i popunje #: erpnext/stock/doctype/delivery_note/mapper.py:82 msgid "All these items have already been invoiced/returned" -msgstr "" +msgstr "Svi ovi artikli su već fakturirani/vraćeni" #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:100 #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js:101 @@ -4144,7 +4144,7 @@ msgstr "Dozvoli Alternativni Artikal" #: erpnext/stock/doctype/item_alternative/item_alternative.py:68 msgid "Allow Alternative Item must be checked on Item {0}" -msgstr "" +msgstr "Dozvoli Alternativni Artikal mora biti odabrano za Artikal {0}" #. Label of the material_consumption (Check) field in DocType 'Manufacturing #. Settings' @@ -4536,7 +4536,7 @@ msgstr "Dozvoljena Transakcija sa" #. Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Allowed Users" -msgstr "" +msgstr "Dopušteni Korisnici" #: erpnext/accounts/doctype/party_link/party_link.py:27 msgid "Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only." @@ -5459,7 +5459,7 @@ msgstr "Termin s" #: erpnext/www/book_appointment/index.js:237 msgid "Appointment created successfully" -msgstr "" +msgstr "Termin je uspješno zakazan" #: erpnext/crm/doctype/appointment/appointment.py:101 msgid "Appointment was created. But no lead was found. Please check the email to confirm" @@ -5505,11 +5505,11 @@ msgstr "Jeste li sigurni da želite izbrisati sve demo podatke?" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:51 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:100 msgid "Are you sure you want to create Reposting Entries?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:66 msgid "Are you sure you want to create a Reposting Entry?" -msgstr "" +msgstr "Jeste li sigurni da želite stvoriti ponovno knjiženje unosa?" #: erpnext/public/js/bom_configurator/bom_configurator.bundle.js:480 msgid "Are you sure you want to delete this Item?" @@ -5603,7 +5603,7 @@ msgstr "Pošto ima dovoljno sirovina, Materijalni Nalog nije potreban za Skladi #: erpnext/stock/doctype/stock_settings/stock_settings.py:250 msgid "As there is reserved stock, you cannot disable {0}." -msgstr "" +msgstr "Budući da postoje rezervirane zalihe, ne možete onemogućiti {0}." #: erpnext/stock/doctype/stock_settings/stock_settings.py:224 #: erpnext/stock/doctype/stock_settings/stock_settings.py:236 @@ -6194,7 +6194,7 @@ msgstr "Dodijeli Imenu" #: erpnext/buying/doctype/purchase_order/purchase_order.js:593 #: erpnext/public/js/controllers/buying.js:555 msgid "Assigning {0} to {1} (row {2})" -msgstr "" +msgstr "Dodjeljuje se {0} {1} (red {2})" #. Label of the filters_section (Section Break) field in DocType 'Service Level #. Agreement' @@ -6220,7 +6220,7 @@ msgstr "U Redu {0}: U Serijskom i Šaržnom Paketu {1} mora imati status dokumen #: erpnext/accounts/services/internal_transfer.py:98 msgid "At Row {0}: The field {1} is mandatory for internal transfer" -msgstr "" +msgstr "U redu {0}: Polje {1} je obavezno za interni prijenos" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:85 msgid "At least one account with exchange gain or loss is required" @@ -6253,7 +6253,7 @@ msgstr "Najmanje jedno od Prodaje ili Nabave mora biti odabrano" #: erpnext/subcontracting/doctype/subcontracting_inward_order/subcontracting_inward_order.py:226 msgid "At least one raw material for Finished Good Item {0} should be customer provided." -msgstr "" +msgstr "Klijent treba osigurati barem jednu sirovinu za Gotov Proizvod {0}." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:60 msgid "At least one raw material item must be present in the stock entry for the type {0}" @@ -6293,7 +6293,7 @@ msgstr "Red {0}: Serijski Broj je obavezan za Artikal {1}" #: erpnext/stock/services/serial_batch_bundle_service.py:498 msgid "At row {0}: Serial and Batch Bundle {1} has already been created. Please remove the values from the serial no or batch no fields." -msgstr "" +msgstr "U redu {0}: Serijski i Šaržni Paket {1} je već izrađen. Ukloni vrijednosti za serijski broj ili broj šarže." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:123 msgid "At row {0}: set Parent Row No for item {1}" @@ -6930,7 +6930,7 @@ msgstr "Sastavnica 1" #: erpnext/manufacturing/doctype/bom/mapper.py:82 msgid "BOM 1 {0} and BOM 2 {1} should not be the same" -msgstr "" +msgstr "Sastavnica 1 {0} i Sastavnica 2 {1} ne smiju biti isti" #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:38 msgid "BOM 2" @@ -7188,7 +7188,7 @@ msgstr "Rekurzija Sastavnice: {1} ne može biti nadređena ili podređena {0}" #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js:81 msgid "BOM update is queued and may take a few minutes. Check {0} for progress." -msgstr "" +msgstr "Ažuriranje Sastavnice je u redu čekanja i može potrajati nekoliko minuta. Provjerite {0} za napredak." #: erpnext/manufacturing/doctype/bom/bom.py:1404 msgid "BOM {0} does not belong to Item {1}" @@ -7351,7 +7351,7 @@ msgstr "Sažetak Bilansa Stanja" #: erpnext/accounts/report/balance_sheet/balance_sheet.py:284 msgid "Balance Sheet requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilansa Stanja zahtijeva da se {0} sinkronizuje s DuckDB-om" #: erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html:13 msgid "Balance Stock Qty" @@ -7514,7 +7514,7 @@ msgstr "Tip Bankovnog Računa" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:443 msgid "Bank Account {0} in Bank Transaction {1} is not matching with Bank Account {2}" -msgstr "" +msgstr "Bankovni Račun {0} u Bankovnoj Transakciji {1} ne odgovara Bankovnim Računu {2}" #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:15 #: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:20 @@ -8096,7 +8096,7 @@ msgstr "Broj Šarže je obavezan" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:3495 msgid "Batch No {0} does not exist" -msgstr "" +msgstr "Broj Šarže {0} ne postoji" #: erpnext/stock/utils.py:625 msgid "Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead." @@ -8108,7 +8108,7 @@ msgstr "Broj Šarže {0} nije prisutan u originalnom {1} {2}, stoga ga ne možet #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:658 msgid "Batch No {0} of Item {1} has negative stock of quantity {2} in the warehouse {3}" -msgstr "" +msgstr "Broj Šarže {0} Artikla {1} ima negativnu količinu {2} u skladištu {3}" #. Label of the batch_no (Int) field in DocType 'BOM Update Batch' #: erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -8177,7 +8177,7 @@ msgstr "Šarža i Serijski Broj" #: erpnext/manufacturing/doctype/work_order/work_order.py:742 msgid "Batch not created for item {0} since it does not have a batch series." -msgstr "" +msgstr "Šarža nije izrađena za artikal {0} jer nema Broj Šarže." #. Description of the 'Automatically Create New Batch' (Check) field in DocType #. 'Item' @@ -8765,7 +8765,7 @@ msgstr "Proknjižena Osnovna Imovina" #: erpnext/accounts/services/gl_validator.py:143 msgid "Books have been closed until the period ending on {0}" -msgstr "" +msgstr "Knjigovodstvo je zatvoreno do kraja razdoblja koje završava {0}" #. Option for the 'Type of Transaction' (Select) field in DocType 'Inventory #. Dimension' @@ -8993,7 +8993,7 @@ msgstr "Proračun se ne može dodijeliti naspram Grupnog Računu {0}" #: erpnext/accounts/doctype/budget/budget.py:165 msgid "Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense" -msgstr "" +msgstr "Proračun se ne može dodijeliti za {0}, jer njegova Kontna Klasa nije Prihod ili Rashod" #: erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py:9 msgid "Budgets" @@ -9347,7 +9347,7 @@ msgstr "Izračunata Razlika Popusta" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:95 msgid "Calculating arrival times" -msgstr "" +msgstr "Izračunavanje vremena dolaska" #. Label of the section_break_11 (Section Break) field in DocType 'Supplier #. Scorecard Period' @@ -9565,7 +9565,7 @@ msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije na #: erpnext/stock/doctype/stock_settings/stock_settings.py:191 msgid "Can't change the valuation method, as there are transactions against some items which do not have their own valuation method" -msgstr "" +msgstr "Ne može se promijeniti način vrijednovanja, jer postoje transakcije za neke artikle koji nemaju vlastiti metod vrijednovanja" #: erpnext/support/doctype/warranty_claim/warranty_claim.py:79 msgid "Cancel Material Visit {0} before cancelling this Warranty Claim" @@ -9644,7 +9644,7 @@ msgstr "Ne može biti artikal fiksne imovine jer je kreiran Registar Zaliha." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:92 #: erpnext/stock/doctype/delivery_trip/delivery_trip.py:219 msgid "Cannot calculate arrival time as the driver address is missing." -msgstr "" +msgstr "Nije moguće izračunati vrijeme dolaska jer nedostaje adresa vozača." #: erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py:117 msgid "Cannot cancel Asset Depreciation Schedule {0} as it has a draft journal entry {1}." @@ -9656,7 +9656,7 @@ msgstr "Ne može se otkazati Unos Zatvaranja Blagajne" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:140 msgid "Cannot cancel Stock Reservation Entry {0}, as it has been used in the work order {1}. Please cancel the work order first or unreserve the stock" -msgstr "" +msgstr "Ne može se otkazati Unos Rezervacije Zaliha {0} jer je korišten u radnom nalogu {1}. Prvo otkaži radni nalog ili poništiti rezervaciju zaliha" #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:275 msgid "Cannot cancel as processing of cancelled documents is pending." @@ -9708,7 +9708,7 @@ msgstr "Nije moguće promijeniti standard valutu tvrtke, jer postoje postojeće #: erpnext/projects/doctype/task/task.py:146 msgid "Cannot complete task {0} as its dependent task {1} is not completed / cancelled." -msgstr "" +msgstr "Nije moguće dovršiti zadatak {0} jer njegov zavisni zadatak {1} nije dovršen / otkazan." #: erpnext/accounts/doctype/cost_center/cost_center.py:61 msgid "Cannot convert Cost Center to ledger as it has child nodes" @@ -9745,7 +9745,7 @@ msgstr "Nije moguće kreirati knjigovodstvene unose naspram onemogućenih račun #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:146 msgid "Cannot create more Subcontracting Orders against the Purchase Order {0}." -msgstr "" +msgstr "Ne može se izraditi više Podugovornih Naloga na osnovu Naloga Nabave {0}." #: erpnext/controllers/sales_and_purchase_return.py:444 msgid "Cannot create return for consolidated invoice {0}." @@ -9840,7 +9840,7 @@ msgstr "Nije moguće spojiti {0} '{1}' u '{2}' jer oba imaju postojeće knjigovo #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:125 msgid "Cannot optimize route as the driver address is missing." -msgstr "" +msgstr "Nije moguće optimizirati rutu jer nedostaje adresa vozača." #: erpnext/manufacturing/doctype/work_order/services/status.py:41 msgid "Cannot produce more Item {0} than Sales Order quantity {1} {2}" @@ -9870,7 +9870,7 @@ msgstr "Ne može se upućivati na broj reda veći ili jednak trenutnom broju red #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:626 msgid "Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.

The Allowed Qty is calculated as follows:
" -msgstr "" +msgstr "Ne može se rezervirati više od Dopuštene Količine {0} {1} za Artikal {2} za {3} {4}.

Dopuštena Količina izračunava se na sljedeći način:
" #: erpnext/accounts/doctype/bank/bank.js:63 msgid "Cannot retrieve link token for update. Check Error Log for more information" @@ -9895,7 +9895,7 @@ msgstr "Nije moguće odabrati tip naknade kao 'Iznos na Prethodnom Redu' ili 'Uk #: erpnext/stock/doctype/item_alternative/item_alternative.py:36 msgid "Cannot set alternative item for the item {0}" -msgstr "" +msgstr "Nije moguće postaviti alternativni artikal za artikal {0}" #: erpnext/selling/doctype/quotation/quotation.py:293 msgid "Cannot set as Lost as Sales Order is made." @@ -10325,7 +10325,7 @@ msgstr "Ručno promijenite ovaj datum da postavite sljedeći datum početka sinh #: erpnext/selling/doctype/customer/customer.py:161 msgid "Changed customer name to '{0}' as '{1}' already exists." -msgstr "" +msgstr "Ime klijenta promijenjeno je u '{0}' jer '{1}' već postoji." #: erpnext/manufacturing/page/bom_comparison_tool/bom_comparison_tool.js:160 msgid "Changes in {0}" @@ -10615,7 +10615,7 @@ msgstr "Podređena tablica nije dopuštena" #: erpnext/projects/doctype/task/task.py:319 msgid "Child Task exists for this Task. You cannot delete this Task." -msgstr "" +msgstr "Za ovaj zadatak postoji podređeni zadatak. Ne možete izbrisati ovaj zadatak." #: erpnext/stock/doctype/warehouse/warehouse_tree.js:21 msgid "Child nodes can be only created under 'Group' type nodes" @@ -11795,11 +11795,11 @@ msgstr "Naziv polja poveznice tvrtke koje se koristi za filtriranje (neobavezno #: erpnext/setup/doctype/company/company.js:239 msgid "Company name does not match" -msgstr "" +msgstr "Naziv tvrtke se ne poklapa" #: erpnext/assets/doctype/asset/asset.py:330 msgid "Company of asset {0} and purchase document {1} does not match." -msgstr "" +msgstr "Tvrtka imovine {0} i dokument o nabavi {1} se ne poklapa." #: erpnext/setup/doctype/employee/employee.py:164 msgid "Company or Personal Email is mandatory when 'Create User Automatically' is enabled" @@ -11839,11 +11839,11 @@ msgstr "Tvrtka {0} ne postoji" #: erpnext/setup/setup_wizard/operations/taxes_setup.py:14 msgid "Company {0} does not exist yet. Taxes setup aborted." -msgstr "" +msgstr "Tvrtka {0} još ne postoji. Postavljanje PDV-a je prekinuto." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:579 msgid "Company {0} does not match with POS Profile Company {1}" -msgstr "" +msgstr "Tvrtka {0} ne odgovara Kasa Profilu Tvrtke {1}" #: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:105 msgid "Company {0} is added more than once" @@ -12319,7 +12319,7 @@ msgstr "Potrošena Količina" #: erpnext/manufacturing/doctype/work_order/services/reservation.py:186 msgid "Consumed Qty {0} cannot be greater than Reserved Qty {1} for item {2}" -msgstr "" +msgstr "Potrošena Količina {0} ne može biti veća od Rezervirane Količine {1} za artikal {2}" #. Label of the consumed_quantity (Data) field in DocType 'Asset Repair #. Consumed Item' @@ -13052,11 +13052,11 @@ msgstr "Centar Troškova {0} ne može se koristiti za dodjelu jer se koristi kao #: erpnext/assets/doctype/asset/asset.py:358 msgid "Cost Center {0} does not belong to Company {1}" -msgstr "" +msgstr "Centar Troška {0} ne pripada Tvrtki {1}" #: erpnext/assets/doctype/asset/asset.py:365 msgid "Cost Center {0} is a group cost center and group cost centers cannot be used in transactions" -msgstr "" +msgstr "Centar Troška {0} je grupni centar troška a grupni centri troška ne mogu se koristiti u transakcijama" #: erpnext/accounts/report/financial_statements.py:685 msgid "Cost Center: {0} does not exist" @@ -13181,7 +13181,7 @@ msgstr "Obračun Troškova i Fakturisanje" #: erpnext/projects/doctype/project/project.js:140 msgid "Costing and Billing fields have been updated" -msgstr "" +msgstr "Polja Troškova i Fakturiranja su ažurirana" #: erpnext/setup/demo.py:78 msgid "Could Not Delete Demo Data" @@ -13210,7 +13210,7 @@ msgstr "Nije moguće pronaći odgovarajuću promjenu koja bi odgovarala razlici: #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:46 #: erpnext/buying/doctype/supplier_scorecard_variable/supplier_scorecard_variable.py:50 msgid "Could not find path for {0}" -msgstr "" +msgstr "Nije moguće pronaći put za {0}" #: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:101 msgid "Could not re-extract the table." @@ -14335,7 +14335,7 @@ msgstr "Trenutna Sastavnica" #: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py:80 msgid "Current BOM and New BOM cannot be the same" -msgstr "" +msgstr "Trenutna i Nova Sastavnica ne mogu biti iste" #. Label of the current_exchange_rate (Float) field in DocType 'Exchange Rate #. Revaluation Account' @@ -17059,11 +17059,11 @@ msgstr "Razlika u kontu stavki u tablici" #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:155 msgid "Difference Account must be an Asset/Liability type account (Temporary Opening), since this Stock Entry is an Opening Entry" -msgstr "" +msgstr "Račun Razlika mora biti račun tipa Imovina/Obveza (Privremeno Početno), budući da je ovaj unos zaliha početni unos" #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:985 msgid "Difference Account must be an Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" -msgstr "" +msgstr "Račun Razlike mora biti račun tipa Imovine/Obveze, budući da je ovo Usklađivanje Zaliha početni unos" #. Label of the difference_amount (Currency) field in DocType 'Payment #. Reconciliation Allocation' @@ -17309,7 +17309,7 @@ msgstr "Onemogućeni artikli se ne mogu odabrati ni u jednoj transakciji." #: erpnext/accounts/services/internal_transfer.py:120 msgid "Disabled pricing rules since this {0} is an internal transfer" -msgstr "" +msgstr "Pravila određivanja cijena onemogućena su jer je ovo {0} interni prijenos" #. Description of the 'Disabled' (Check) field in DocType 'Supplier' #: erpnext/buying/doctype/supplier/supplier.json @@ -17318,7 +17318,7 @@ msgstr "Onemogućeni dobavljači su skriveni od odabira u novim transakcijama, a #: erpnext/accounts/services/internal_transfer.py:136 msgid "Disabled tax included prices since this {0} is an internal transfer" -msgstr "" +msgstr "Cijene s PDV-om onemogućene jer je ovo {0} interni prijenos" #: erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py:82 msgid "Disabled template must not be default template" @@ -17569,7 +17569,7 @@ msgstr "Popust mora biti manji od 100%" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:3090 msgid "Discount of {0} applied as per Payment Term" -msgstr "" +msgstr "Popust od {0} primijenjen prema Uvjetima Plaćanja" #. Label of the section_break_18 (Section Break) field in DocType 'Pricing #. Rule' @@ -17934,7 +17934,7 @@ msgstr "Želiš li podnijeti unos zaliha?" #: erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py:50 #: erpnext/selling/report/sales_partner_commission_summary/test_sales_partner_commission_summary.py:22 msgid "DocType can be one of {0}" -msgstr "" +msgstr "DocType može biti jedan od {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:182 #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:458 @@ -18659,7 +18659,7 @@ msgstr "Verifikacija e-pošte nije uspjela." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:20 msgid "Emails queued" -msgstr "" +msgstr "E-pošta u redu čekanja" #. Label of the emergency_contact_details (Section Break) field in DocType #. 'Employee' @@ -18938,7 +18938,7 @@ msgstr "Omogući Evropski Pristup" #. 'CRM Settings' #: erpnext/crm/doctype/crm_settings/crm_settings.json msgid "Enable Frappe CRM Data Synchronization" -msgstr "" +msgstr "Omogući sinkronizaciju podataka Prodajne Podrške" #. Label of the enable_fuzzy_matching (Check) field in DocType 'Accounts #. Settings' @@ -19397,7 +19397,7 @@ msgstr "Unesi {0} iznos." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:170 msgid "Enter {0} name." -msgstr "" +msgstr "Unesi {0} ime." #: erpnext/setup/setup_wizard/data/industry_type.txt:22 msgid "Entertainment & Leisure" @@ -19496,15 +19496,15 @@ msgstr "Greška prilikom ponovnog knjiženja vrijednosti artikla" #: erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py:175 msgid "Error: This asset already has {0} depreciation periods booked. The `depreciation start` date must be at least {1} periods after the `available for use` date. Please correct the dates accordingly." -msgstr "" +msgstr "Pogreška: Ova imovina već ima rezerviranih {0} razdoblja amortizacije. Datum `početka amortizacije` mora biti najmanje {1} razdoblja nakon datuma `raspoloživosti za upotrebu`. Molimo ispravite datume u skladu s tim." #: erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py:93 msgid "Error: {0}" -msgstr "" +msgstr "Pogreška: {0}" #: erpnext/accounts/doctype/payment_entry/payment_entry.js:980 msgid "Error: {0} is a mandatory field" -msgstr "" +msgstr "Pogreška: {0} je obavezno polje" #. Label of the errors_notification_section (Section Break) field in DocType #. 'Stock Reposting Settings' @@ -20197,7 +20197,7 @@ msgstr "Neuspješni Unosi" #: erpnext/utilities/doctype/video_settings/video_settings.py:35 msgid "Failed to authenticate the API key. Please check the error logs." -msgstr "" +msgstr "Autentifikacija API ključa nije uspjela. Provjerite zapisnike pogrešaka." #: erpnext/setup/setup_wizard/setup_wizard.py:37 #: erpnext/setup/setup_wizard/setup_wizard.py:38 @@ -21176,11 +21176,11 @@ msgstr "Za Radni Nalog" #: erpnext/controllers/status_updater.py:292 msgid "For an item {0}, quantity must be a negative number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti negativan broj" #: erpnext/controllers/status_updater.py:289 msgid "For an item {0}, quantity must be a positive number" -msgstr "" +msgstr "Za Artikal {0}, količina mora biti pozitivan broj" #. Description of the 'Income Account' (Link) field in DocType 'Dunning' #: erpnext/accounts/doctype/dunning/dunning.json @@ -21214,11 +21214,11 @@ msgstr "Za individualnog Dobavljača" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:379 msgid "For item {0}, only {1} assets have been created or linked to {2}. Please create or link {3} more assets with the respective document." -msgstr "" +msgstr "Za artikal {0}, samo {1} imovina je stvorena ili povezana s {2}. Stvori ili poveži još {3} imovine s odgovarajućim dokumentom." #: erpnext/controllers/status_updater.py:302 msgid "For item {0}, rate must be a positive number. To allow negative rates, enable {1} in {2}" -msgstr "" +msgstr "Za artikal {0}, cijena mora biti pozitivan broj. Da biste omogućili negativne cijene, omogućite {1} u {2}" #. Description of the 'Do not fetch incoming rate from Serial No' (Check) field #. in DocType 'Stock Reposting Settings' @@ -21232,7 +21232,7 @@ msgstr "Za operaciju {0} u redu {1}, molimo dodajte sirovine ili postavite Sasta #: erpnext/manufacturing/doctype/work_order/mapper.py:379 msgid "For operation {0}: Quantity ({1}) can not be greater than pending quantity ({2})" -msgstr "" +msgstr "Za Operaciju {0}: Količina ({1}) ne može biti veća od količine na čekanju ({2})" #: erpnext/projects/doctype/project/project.js:208 msgid "For project - {0}, update your status" @@ -21278,7 +21278,7 @@ msgstr "Za praktičnost Klienta, ovi kodovi se mogu koristiti u formatima za isp #: erpnext/stock/serial_batch_bundle.py:1234 msgid "For the item {0}, the Available qty {1} is less than the Required Qty {2} in the warehouse {3}. Please add sufficient qty in the warehouse." -msgstr "" +msgstr "Za artikal {0}, Raspoloživa Količina {1} je manja od Zatražene Količine {2} u skladištu {3}. Dodaj dovoljnu količinu u skladište." #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:893 msgid "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." @@ -21381,11 +21381,11 @@ msgstr "Podrška Prodaje" #. Name of a DocType #: erpnext/crm/doctype/frappe_crm_allowed_user/frappe_crm_allowed_user.json msgid "Frappe CRM Allowed User" -msgstr "" +msgstr "Dozvoljeni korisnik Prodajne Podrške" #: erpnext/crm/frappe_crm_api.py:168 msgid "Frappe CRM data synchronization is not enabled on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Sinkronizacija podataka Prodajne Podrške nije omogućena U Sustavu. Obrati se Upravitelju Sustava." #: erpnext/setup/install.py:232 msgid "Frappe School" @@ -22057,7 +22057,7 @@ msgstr "Dužina napomena Knjigovodstvenog Registra" #: erpnext/accounts/report/general_ledger/general_ledger.py:829 msgid "General Ledger requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Knjigovodstveni Registar zahtijeva da se {0} sinkronizuje sa DuckDB-om" #. Label of the gs (Section Break) field in DocType 'Item Group' #: erpnext/setup/doctype/item_group/item_group.json @@ -24098,7 +24098,7 @@ msgstr "Uvezi Fakture" #. Import' #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json msgid "Import MT940 Format" -msgstr "" +msgstr "Uvezi MT940 Format" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:144 msgid "Import Successful" @@ -25495,7 +25495,7 @@ msgstr "Nevažeće Skladište" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:460 msgid "Invalid amount in accounting entries of {0} {1} for Account {2}: {3}" -msgstr "" +msgstr "Nevažeći iznos u knjigovodstvenim unosima {0} {1} za račun {2}: {3}" #: erpnext/accounts/doctype/pricing_rule/pricing_rule.py:312 msgid "Invalid condition expression" @@ -27846,7 +27846,7 @@ msgstr "Artikal {0} se nemože naručiti više od {1} u odnosu na Ugovorni Nalog #: erpnext/stock/services/internal_transfer.py:104 msgid "Item {0} cannot be received in more than {1} qty against the {2} {3}" -msgstr "" +msgstr "Artikal {0} ne može se primiti u količini većoj od {1} u odnosu na {2} {3}" #: erpnext/assets/doctype/asset/asset.py:343 #: erpnext/stock/doctype/item/item.py:693 @@ -27892,7 +27892,7 @@ msgstr "Artikal {0} zanemaren jer nije artikal na zalihama" #: erpnext/stock/get_item_details.py:359 msgid "Item {0} is a template, please select one of its variants" -msgstr "" +msgstr "Artikal {0} je predložak, odaberite jednu od njezinih varijanti" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:614 msgid "Item {0} is already reserved/delivered against Sales Order {1}." @@ -28010,7 +28010,7 @@ msgstr "Artikal: {0} ne postoji u sustavu" #: erpnext/manufacturing/doctype/bom/bom.py:970 msgid "Item: {0} with Stock UOM: {1} cannot have fractional process loss qty as UOM {2} is a whole number." -msgstr "" +msgstr "Artikal: {0} s jedinicom zalihe: {1} ne može imati frakcijsku količinu gubitaka u procesu jer je jedinica mjere {2} cijeli broj." #. Label of a Card Break in the Buying Workspace #. Label of a Workspace Sidebar Item @@ -28217,7 +28217,7 @@ msgstr "Radne Kartice {0} je završen" #: erpnext/manufacturing/doctype/job_card/job_card.py:1416 msgid "Job Card {0}: As per the sequence of the operations in the work order {1}, complete the operation {2} before the operation {3}." -msgstr "" +msgstr "Radna Kartica {0}: Prema redoslijedu operacija u radnom nalogu {1}, dovršite operaciju {2} prije operacije {3}." #. Label of the dashboard_tab (Tab Break) field in DocType 'Workstation' #: erpnext/manufacturing/doctype/workstation/workstation.json @@ -28292,11 +28292,11 @@ msgstr "Radna Kartica {0} kreirana" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:106 msgid "Job paused" -msgstr "" +msgstr "Posao Pauziran" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:64 msgid "Job started" -msgstr "" +msgstr "Posao Započet" #: erpnext/utilities/bulk_transaction.py:72 msgid "Job: {0} has been triggered for processing failed transactions" @@ -28645,7 +28645,7 @@ msgstr "Prošla Fiskalna Godina" #: erpnext/accounts/doctype/account/account.py:673 msgid "Last GL Entry update was done {0}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying." -msgstr "" +msgstr "Posljednje ažuriranje Knjigovodstvenog Registra je obavljeno {0}. Ova operacija nije dopuštena dok se sustav aktivno koristi. Pričekaj 5 minuta prije ponovnog pokušaja." #. Label of the last_integration_date (Date) field in DocType 'Bank Account' #: erpnext/accounts/doctype/bank_account/bank_account.json @@ -29163,7 +29163,7 @@ msgstr "Povezivanje s klijentom nije uspjelo. Molimo pokušajte ponovo." #: erpnext/selling/doctype/customer/customer.js:282 msgid "Linking to Supplier failed. Please try again." -msgstr "" +msgstr "Povezivanje sa Dobavljačem nije uspjelo. Pokušaj ponovo." #: erpnext/accounts/report/financial_ratios/financial_ratios.js:55 #: erpnext/accounts/report/financial_ratios/financial_ratios.py:152 @@ -30731,7 +30731,7 @@ msgstr "Materijali su već primljeni naspram {0} {1}" #: erpnext/manufacturing/doctype/job_card/job_card.py:189 #: erpnext/manufacturing/doctype/job_card/job_card.py:903 msgid "Materials need to be transferred to the work in progress warehouse for the job card {0}" -msgstr "" +msgstr "Materijali se moraju prenijeti u skladište nedovršene proizvodnje za radnu karticu {0}" #. Label of the max_amount (Currency) field in DocType 'Bank Transaction Rule' #. Label of the max_amount (Currency) field in DocType 'Promotional Scheme @@ -31618,7 +31618,7 @@ msgstr "Više Računa (Predložak Naloga Knjiženja)" #: erpnext/selling/doctype/customer/customer.py:443 msgid "Multiple Loyalty Programs found for Customer {0}. Please select manually." -msgstr "" +msgstr "Višei Program Vjernosti pronađeno je za Klijenta {0}. Odaberi ručno." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:253 msgid "Multiple POS Opening Entry" @@ -31626,7 +31626,7 @@ msgstr "Višestruki Unos Otvaranja Blagajne" #: erpnext/accounts/doctype/pricing_rule/utils.py:345 msgid "Multiple Price Rules exist with same criteria, please resolve conflict by assigning priority. Price Rules: {0}" -msgstr "" +msgstr "Postoji više pravila o cijenama s istim kriterijima, molimo riješite sukob dodjeljivanjem prioriteta. Pravila o cijenama: {0}" #. Option for the 'Loyalty Program Type' (Select) field in DocType 'Loyalty #. Program' @@ -32289,7 +32289,7 @@ msgstr "Novi Radni Prostor" #: erpnext/selling/doctype/customer/customer.py:408 msgid "New credit limit is less than current outstanding amount for the customer. Credit limit has to be at least {0}" -msgstr "" +msgstr "Novo kreditno ograničenje je manje od trenutnog nepodmirenog iznosa klijenta. Kreditno ograničenje mora biti najmanje {0}" #. Description of the 'Bill Even If Previous Invoice Unpaid' (Check) field in #. DocType 'Subscription' @@ -32299,7 +32299,7 @@ msgstr "Nove fakture će se generirati prema rasporedu čak i ako su trenutne fa #: erpnext/support/doctype/issue/issue.js:126 msgid "New issue created: {0}" -msgstr "" +msgstr "Novi zahtjev stvoren: {0}" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js:261 msgid "New release date should be in the future" @@ -32383,7 +32383,7 @@ msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:146 msgid "No Delivery Note selected for Customer {0}" -msgstr "" +msgstr "Nije odabrana Dostavnica za Klijenta {0}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:767 msgid "No DocTypes in To Delete list. Please generate or import the list before submitting." @@ -32548,7 +32548,7 @@ msgstr "Nisu pronađeni kontakti s e-poštom." #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:164 msgid "No customers found with selected options." -msgstr "" +msgstr "Nisu pronađeni Klijenti sa odabranim opcijama." #: erpnext/selling/page/sales_funnel/sales_funnel.js:137 msgid "No data for this period" @@ -32769,7 +32769,7 @@ msgstr "Nije pronađen nijedan zapis" #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:22 #: erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js:39 msgid "No records for these settings." -msgstr "" +msgstr "Nema zapisa za ove postavke." #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:773 msgid "No records found in Allocation table" @@ -33268,7 +33268,7 @@ msgstr "Numeričke Vrijednosti" #: erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py:88 msgid "Numero has not been set in the XML file" -msgstr "" +msgstr "Broj nije postavljen u XML datoteci" #. Option for the 'Blood Group' (Select) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json @@ -33444,11 +33444,11 @@ msgstr "Nakon postavljanja, ova faktura će biti na čekanju do postavljenog dat #: erpnext/manufacturing/doctype/work_order/work_order.js:763 msgid "Once the Work Order is Closed, it cannot be resumed." -msgstr "" +msgstr "Nakon što je Radni Nalog Zatvoren, ne može se ponovo otvoriti." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:39 msgid "One customer can be part of only a single Loyalty Program." -msgstr "" +msgstr "Jedan Klijent može biti dio samo jednog Programa Lojalnosti." #. Option for the 'Status' (Select) field in DocType 'Subcontracting Inward #. Order' @@ -34045,7 +34045,7 @@ msgstr "Operacija {0} ne pripada radnom nalogu {1}" #: erpnext/manufacturing/doctype/workstation/workstation.py:453 msgid "Operation {0} is longer than any available working hours in workstation {1}, break down the operation into multiple operations" -msgstr "" +msgstr "Operacija {0} traje duže od bilo kojeg raspoloživog radnog vremena na radnoj stanici {1}, podijelite operaciju na više operacija" #. Label of the operations (Table) field in DocType 'BOM' #. Label of the operations_section_section (Section Break) field in DocType @@ -34232,7 +34232,7 @@ msgstr "Optimiziraj Rutu" #: erpnext/stock/doctype/delivery_trip/delivery_trip.js:128 msgid "Optimizing route" -msgstr "" +msgstr "Optimizacija rute" #: erpnext/manufacturing/doctype/work_order/work_order.js:1029 msgid "Optional. Select a specific manufacture entry to reverse." @@ -34693,7 +34693,7 @@ msgstr "Preko Odbitka" #: erpnext/accounts/services/billing_validation.py:56 msgid "Overbilling of {0} ignored because you have {1} role." -msgstr "" +msgstr "Prekomjerno Fakturiranje {0} zanemareno jer imate {1} ulogu." #: erpnext/controllers/status_updater.py:519 msgid "Overbilling of {0} {1} ignored for item {2} because you have {3} role." @@ -34815,7 +34815,7 @@ msgstr "Verifikat Zatvaranje Perioda" #. Label of the pcv_job_timeout (Int) field in DocType 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "PCV Job Timeout (seconds)" -msgstr "" +msgstr "Vremensko Ograničenje Zadatka Završnog Verifikata Razdoblja (sekunde)" #: erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.js:35 msgid "PCV Paused" @@ -34963,7 +34963,7 @@ msgstr "Faktura Blagajne nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:130 msgid "POS Invoice isn't created by user {0}" -msgstr "" +msgstr "Fakturu Blagajne nije kreirao korisnik {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:208 msgid "POS Invoice should have the field {0} checked." @@ -35087,7 +35087,7 @@ msgstr "Korisnik Profila Blagajne" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:124 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:189 msgid "POS Profile doesn't match {0}" -msgstr "" +msgstr "Profil Blagajne ne poklapa se s {0}" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:210 msgid "POS Profile is mandatory to mark this invoice as POS Transaction." @@ -35099,19 +35099,19 @@ msgstr "Kasa Profil {0} ne može se onemogućiti jer su u tijeku Kasa sesije." #: erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py:62 msgid "POS Profile {0} contains Mode of Payment {1}. Please remove them to disable this mode." -msgstr "" +msgstr "Profil Blagajne {0} sadrži ovaj način plaćanja {1}. Uklonite ga da onemogućite ovaj način." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:58 msgid "POS Profile {0} does not belong to company {1}" -msgstr "" +msgstr "Profil Blagajne {0} ne pripada tvrtki {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:47 msgid "POS Profile {0} does not exist." -msgstr "" +msgstr "Profil Blagajne {0} ne postoji." #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:54 msgid "POS Profile {0} is disabled." -msgstr "" +msgstr "Profil Blagajne {0} je onemogućen." #. Name of a report #: erpnext/accounts/report/pos_register/pos_register.json @@ -36059,7 +36059,7 @@ msgstr "Stranka je Obavezna" #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:69 msgid "Party is required to create a payment entry." -msgstr "" +msgstr "Stranka je obavezna za izradu unosa plaćanja." #: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:66 msgid "Party type is required to create a payment entry." @@ -36766,7 +36766,7 @@ msgstr "Tip Plaćanja" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:624 msgid "Payment Type must be one of Receive, Pay, or Internal Transfer" -msgstr "" +msgstr "Tip Plaćanja mora biti Uplata, Isplata i Interni Prijenos" #. Label of the payment_url (Data) field in DocType 'Payment Request' #: erpnext/accounts/doctype/payment_request/payment_request.json @@ -37723,7 +37723,7 @@ msgstr "Dodaj račun za pravilo bankovnog unosa." #: erpnext/public/js/utils/serial_no_batch_selector.js:663 msgid "Please add at least one Serial No / Batch No" -msgstr "" +msgstr "Dodaj barem jedan Serijski Broj / Broj Šarže" #: erpnext/stock/doctype/item/item.js:914 msgid "Please add at least one row in Item Defaults with a Company before setting opening stock." @@ -37731,7 +37731,7 @@ msgstr "Dodaj barem jedan red u Postavke Artikala sa tvrtkom prije postavljanja #: erpnext/crm/doctype/crm_settings/crm_settings.py:51 msgid "Please add at least one user on Allowed Users to allow Data Synchronization from Frappe CRM site." -msgstr "" +msgstr "Dodaj barem jednog korisnika na popis Dopušteni Porisnici kako biste omogućili Sinkronizaciju Podataka s Prodajnom Podrškom." #: erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py:84 msgid "Please add the Bank Account column" @@ -37823,7 +37823,7 @@ msgstr "Konfiguriraj račune za pravilo bankovnog unosa." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:345 msgid "Please contact any of the following users for this transaction." -msgstr "" +msgstr "Za ovu transakciju obratite se bilo kojem od sljedećih korisnika." #: erpnext/selling/doctype/customer/customer.py:534 msgid "Please contact any of the following users to extend the credit limits for {0}: {1}" @@ -37895,7 +37895,7 @@ msgstr "Omogući {0} u {1}." #: erpnext/controllers/selling_controller.py:872 msgid "Please enable {0} in {1} to allow same item in multiple rows" -msgstr "" +msgstr "Omogući {0} u {1} kako biste dopustili isti artikal u više redova" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:378 msgid "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." @@ -37907,11 +37907,11 @@ msgstr "Potvrdi da je {0} račun {1} Troškovni račun. Možete promijeniti vrst #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:758 msgid "Please ensure {0} account is a Balance Sheet account." -msgstr "" +msgstr "Provjeri da li je račun {0} račun Bilance Stanja." #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:768 msgid "Please ensure {0} account {1} is a Receivable account." -msgstr "" +msgstr "Provjeri da li je {0} račun {1} račun Potraživanja." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:140 msgid "Please enter Difference Account or set default Stock Adjustment Account for company {0}" @@ -38118,7 +38118,7 @@ msgstr "Molimo vas da generirate popis za brisanje prije podnošenja" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:67 msgid "Please import accounts against parent company or enable {0} in company master." -msgstr "" +msgstr "Uvezi račune naspram matične tvrtkea ili omogući {0} u Postavkama Tvrtke." #: erpnext/setup/doctype/employee/employee.py:291 msgid "Please make sure the employees above report to another Active employee." @@ -38216,7 +38216,7 @@ msgstr "Odaberi Tvrtku" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js:139 #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:76 msgid "Please select Company and Posting Date to get entries" -msgstr "" +msgstr "Odaberi Tvrtku i Datum Knjiženja da biste preuzeli unose" #: erpnext/accounts/doctype/journal_entry/journal_entry.js:435 #: erpnext/manufacturing/doctype/plant_floor/plant_floor.js:28 @@ -38249,7 +38249,7 @@ msgstr "Odaberi Kod Artikla" #: erpnext/selling/doctype/sales_order/sales_order.js:1756 msgid "Please select Items from the Table" -msgstr "" +msgstr "Odaberi artikle iz Tablice" #: erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py:55 msgid "Please select Maintenance Status as Completed or remove Completion Date" @@ -38398,7 +38398,7 @@ msgstr "Odaberi red za kreiranje Unosa Ponovnog Knjiženje" #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js:146 msgid "Please select a supplier" -msgstr "" +msgstr "Odaberi Dobavljača" #: erpnext/accounts/report/purchase_register/purchase_register.py:37 msgid "Please select a supplier for fetching payments." @@ -38410,7 +38410,7 @@ msgstr "Odaberi važeći Nalog Nabave koji je konfigurisan za Podugovor." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:200 msgid "Please select a valid document type." -msgstr "" +msgstr "Odaberi valjani tip dokumenta." #: erpnext/selling/doctype/quotation/quotation.js:245 msgid "Please select a value for {0} quotation_to {1}" @@ -38430,7 +38430,7 @@ msgstr "Molimo odaberite barem jedan filter: Šifra Artikla, Šarža ili Serijsk #: erpnext/selling/doctype/sales_order/sales_order.js:1368 msgid "Please select at least one item to continue" -msgstr "" +msgstr "Odaberi jedan artikal za nastavak" #: erpnext/buying/doctype/purchase_order/services/drop_ship.py:17 msgid "Please select at least one item to update delivered quantity." @@ -38438,7 +38438,7 @@ msgstr "Molimo odaberite barem jedan artikal za ažuriranje dostavljene količin #: erpnext/manufacturing/doctype/work_order/work_order.js:392 msgid "Please select at least one operation to create Job Card" -msgstr "" +msgstr "Odaberi barem jednu operaciju za stvaranje Radne Kartice" #: erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js:33 msgid "Please select at least one row to fix" @@ -38504,7 +38504,7 @@ msgstr "Odaberi Tvrtku" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.js:65 msgid "Please select the Multiple Tier Program type for more than one collection rule." -msgstr "" +msgstr "Odaberi Tip Višeslojnog Programa za više od jednog pravila prikupljanja." #: erpnext/stock/doctype/item/item.js:433 msgid "Please select the Warehouse first" @@ -38566,7 +38566,7 @@ msgstr "Postavi Račun u Skladištu {0} ili Standard Račun Zaliha u Kompaniji { #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:333 msgid "Please set Accounting Dimension {0} in {1}" -msgstr "" +msgstr "Postavi Knjigovodstvenu Dimenziju {0} u {1}" #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:23 #: erpnext/accounts/doctype/ledger_merge/ledger_merge.js:34 @@ -38608,7 +38608,7 @@ msgstr "Postavi Račun Osnovne Imovine u Kategoriju Imovine {0}" #: erpnext/accounts/doctype/purchase_invoice/services/expense_account.py:151 msgid "Please set Fixed Asset Account in {0} against {1}." -msgstr "" +msgstr "Postavi Račun Osnovnih Sredstava u {0} na {1}." #: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:296 msgid "Please set Parent Row No for item {0}" @@ -38645,7 +38645,7 @@ msgstr "Postavi Tvrtku" #: erpnext/assets/doctype/asset/asset.py:374 msgid "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {0}" -msgstr "" +msgstr "Postavi Centar Troškova za Imovinu ili postavite Centar Troškova Amortizacije za {0}" #: erpnext/stock/doctype/item/item.py:339 #: erpnext/stock/doctype/item/item.py:1623 @@ -38699,11 +38699,11 @@ msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Način Plaćanja {0}" #: erpnext/accounts/doctype/pos_profile/pos_profile.py:207 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:367 msgid "Please set default Cash or Bank account in Mode of Payments {0}" -msgstr "" +msgstr "Postavi Standard Gotovinski ili Bankovni Račun za Načine Plaćanja {0}" #: erpnext/accounts/utils.py:2568 msgid "Please set default Exchange Gain/Loss Account in Company {0}" -msgstr "" +msgstr "Postavi Standard Račun Rezultata od Kursnih Razlika u {0}" #: erpnext/assets/doctype/asset_repair/services/gl_composer.py:92 msgid "Please set default Expense Account in Company {0}" @@ -38842,11 +38842,11 @@ msgstr "Navedi od/Do Raspona" #: erpnext/public/js/controllers/transaction.js:2634 msgid "Please specify {0}. It is needed to fetch Item Details." -msgstr "" +msgstr "Navedi {0}. Potrebno je za preuzimanje Detalja Artikla." #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:142 msgid "Please submit Purchase Order {0} before proceeding." -msgstr "" +msgstr "Podnesite Nalog Nabave {0} prije nego što nastavite." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:276 msgid "Please try again in an hour." @@ -39080,7 +39080,7 @@ msgstr "Datuma Knjiženja" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:263 #: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:146 msgid "Posting Date cannot be a future date" -msgstr "" +msgstr "Datum Knjiženja ne može biti budući datum" #. Label of the exchange_gain_loss_posting_date (Select) field in DocType #. 'Accounts Settings' @@ -39283,7 +39283,7 @@ msgstr "Uplaćeni Troškovi" #: erpnext/accounts/report/general_ledger/general_ledger.py:682 msgid "Presentation Currency cannot be {0}, when {1} is enabled." -msgstr "" +msgstr "Valuta prikaza ne može biti {0}, kada je omogućen {1}." #: erpnext/setup/setup_wizard/data/designation.txt:24 msgid "President" @@ -39957,7 +39957,7 @@ msgstr "Prioriteti" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:61 msgid "Priority cannot be less than 1." -msgstr "" +msgstr "Prioritet ne može biti manji od 1." #: erpnext/support/doctype/service_level_agreement/service_level_agreement.py:764 msgid "Priority has been changed to {0}." @@ -40523,7 +40523,7 @@ msgstr "Bilans Uspjeha" #: erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py:215 msgid "Profit and Loss Statement requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Bilanca Uspjeha zahtijeva da se {0} sinkronizuje s DuckDB-om" #. Label of the heading_cppb (Heading) field in DocType 'Bisect Accounting #. Statements' @@ -41282,7 +41282,7 @@ msgstr "Nalog Nabave Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:466 msgid "Purchase Order Required for item {0}" -msgstr "" +msgstr "Nalog Nabave je obavezan za artikal {0}" #. Name of a report #. Label of a chart in the Buying Workspace @@ -41342,7 +41342,7 @@ msgstr "Nalozi Nabave za Primitak" #: erpnext/controllers/accounts_controller.py:1236 msgid "Purchase Orders {0} are unlinked" -msgstr "" +msgstr "Nabavni Nalozi {0} nisu povezani" #: erpnext/stock/report/item_prices/item_prices.py:59 msgid "Purchase Price List" @@ -41432,7 +41432,7 @@ msgstr "Nabavni Račun je Obavezan" #: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:488 msgid "Purchase Receipt Required for item {0}" -msgstr "" +msgstr "Račun Nabave je obavezan za artikal {0}" #. Label of a Link in the Buying Workspace #. Name of a report @@ -41452,7 +41452,7 @@ msgstr "Statistika Nabavnog Računa " #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.js:356 msgid "Purchase Receipt does not have any Item for which Retain Sample is enabled." -msgstr "" +msgstr "Račun Nabave nema nijedan artikal za koju je omogućeno Zadržavanje Uzorka." #: erpnext/subcontracting/doctype/subcontracting_receipt/mapper.py:135 msgid "Purchase Receipt {0} created." @@ -42483,7 +42483,7 @@ msgstr "Količina za Skeniranje" #: erpnext/stock/doctype/stock_entry/stock_entry.py:910 msgid "Quantity {0} should not be greater than allowed quantity {1}" -msgstr "" +msgstr "Količina {0} ne smije biti veća od dopuštene količine {1}" #. Name of a UOM #: erpnext/setup/setup_wizard/data/uom_data.json @@ -42935,7 +42935,7 @@ msgstr "PDV Stopa" #: erpnext/accounts/services/child_item_update.py:515 msgid "Rate of '{0}' items cannot be changed" -msgstr "" +msgstr "Cijena '{0}' artikala ne može se mijenjati" #. Label of the rate_of_depreciation (Percent) field in DocType 'Asset #. Depreciation Schedule' @@ -43674,7 +43674,7 @@ msgstr "Zabilježite prijenos između dva bankovna računa" #: erpnext/stock/doctype/item_alternative/item_alternative.py:84 msgid "Record already exists for the item {0}" -msgstr "" +msgstr "Zapis za artikal {0} već postoji" #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:513 #: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:519 @@ -44099,7 +44099,7 @@ msgstr "Odbijeno Skladište" #: erpnext/public/js/utils/serial_no_batch_selector.js:671 msgid "Rejected Warehouse and Accepted Warehouse cannot be the same." -msgstr "" +msgstr "Odbijeno i Prihvaćeno Skladište ne mogu biti ista." #: erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py:23 #: erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py:14 @@ -44528,11 +44528,11 @@ msgstr "Datoteke Podataka Ponovnog Knjiženja" #: erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js:47 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:96 msgid "Reposting Entries will change the value of accounts Stock In Hand, and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje unosa promijenit će vrijednost računa Zalihe na Raspolaganju i Troškovi zaliha u izvješću Probna Bilanca, a također će promijeniti i vrijednost stanja u izvješću Stanja Zaliha." #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js:62 msgid "Reposting Entry will change the value of accounts Stock In Hand and Stock Expenses in the Trial Balance report and will also change the Balance Value in the Stock Balance report." -msgstr "" +msgstr "Ponovno knjiženje će promijeniti vrijednost računa Zalihe na Raspolaganju i Troškovi Zaliha u izvješću Probna Bilanca, a također će promijeniti i vrijednost stanja u izvješću Stanju Zaliha." #. Label of the reposting_info_section (Section Break) field in DocType 'Repost #. Item Valuation' @@ -44919,7 +44919,7 @@ msgstr "Rezervno Skladište" #: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:178 msgid "Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {0}." -msgstr "" +msgstr "Rezervno Skladište mora biti različito od Dobavljačevog Skladišta za Isporučeni Artikal {0}." #: erpnext/manufacturing/doctype/production_plan/production_plan.js:287 msgid "Reserve for Raw Materials" @@ -45522,7 +45522,7 @@ msgstr "Povrati" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:359 msgid "Revaluation Journal: {0}" -msgstr "" +msgstr "Žurnal Revalorizacije: {0}" #: erpnext/accounts/report/accounts_payable/accounts_payable.js:151 #: erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js:113 @@ -46038,7 +46038,7 @@ msgstr "Red #{0}: Broj Šarže {1} je već odabran." #: erpnext/controllers/subcontracting_inward_controller.py:443 msgid "Row #{0}: Batch No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)." -msgstr "" +msgstr "Red #{0}: Šaržni Broj(evi) {1} nije u povezanom Podugovaračkom Nalogu. Odaberi važeće Šaržne broj(eve)." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:880 msgid "Row #{0}: Cannot allocate more than {1} against payment term {2}" @@ -46130,7 +46130,7 @@ msgstr "Red #{0}: Kumulativni prag ne može biti manji od praga pojedinačne tra #: erpnext/assets/doctype/asset_category/asset_category.py:66 msgid "Row #{0}: Currency of {1} - {2} does not match company currency." -msgstr "" +msgstr "Red #{0}: Valuta od {1} do {2} ne odgovara valuti tvrtke." #: erpnext/controllers/subcontracting_inward_controller.py:91 msgid "Row #{0}: Customer Provided Item {1} against Subcontracting Inward Order Item {2} ({3}) cannot be added multiple times." @@ -46184,7 +46184,7 @@ msgstr "Red #{0}: Duplikat unosa u Referencama {1} {2}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:113 msgid "Row #{0}: Either Party ID or Party Name is required" -msgstr "" +msgstr "Red #{0}: Obavezan je ili ID Stranke ili Naziv Stranke" #: erpnext/selling/doctype/sales_order/sales_order.py:270 msgid "Row #{0}: Expected Delivery Date cannot be before Purchase Order Date" @@ -46200,7 +46200,7 @@ msgstr "Red #{0}: Račun troškova {1} nije važeći za Fakturu Nabave {2}. Dopu #: erpnext/assets/doctype/asset/asset.py:421 msgid "Row #{0}: Finance Book should not be empty since you're using multiple." -msgstr "" +msgstr "Red #{0}: Finansijski Registar ne smije biti prazan jer ih koristite više." #: erpnext/selling/doctype/sales_order/services/subcontracting.py:40 msgid "Row #{0}: Finished Good Item Qty can not be zero" @@ -46208,7 +46208,7 @@ msgstr "Red #{0}: Količina gotovog proizvoda artikla ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:39 msgid "Row #{0}: Finished Good Item Qty cannot be zero" -msgstr "" +msgstr "Red #{0}: Količina gotovog proizvoda artikla ne može biti nula" #: erpnext/buying/doctype/purchase_order/services/subcontracting.py:21 #: erpnext/selling/doctype/sales_order/services/subcontracting.py:20 @@ -46259,7 +46259,7 @@ msgstr "Red #{0}: Polja Od i Do su obavezna" #: erpnext/stock/doctype/pick_list/pick_list.py:650 msgid "Row #{0}: Item Code is Mandatory" -msgstr "" +msgstr "Red #{0}: Šifra Artikla je obavezna" #: erpnext/public/js/utils/barcode_scanner.js:427 msgid "Row #{0}: Item added" @@ -46316,15 +46316,15 @@ msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može s #: erpnext/controllers/subcontracting_inward_controller.py:80 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted, add another row instead." -msgstr "" +msgstr "Red #{0}: Artikal {1} se ne slaže. Promjena koda artikla nije dopuštena, umjesto toga dodaj još jedan red." #: erpnext/controllers/subcontracting_inward_controller.py:129 msgid "Row #{0}: Item {1} mismatch. Changing the item code is not permitted." -msgstr "" +msgstr "Red #{0}: Artikal {1} se ne slaže. Promjena koda artikla nije dopuštena." #: erpnext/stock/doctype/stock_entry/services/subcontracting.py:94 msgid "Row #{0}: Item {1} not found in 'Raw Materials Supplied' table in {2} {3}" -msgstr "" +msgstr "Red #{0}: Artikal {1} nije pronađen u tablici 'Isporučene Sirovine' u {2} {3}" #: erpnext/stock/doctype/stock_entry/services/disassemble.py:115 msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows." @@ -46365,19 +46365,19 @@ msgstr "Red #{0}: Prekomjerna potrošnja Klijent Dostavljenog Artikla {1} u odno #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:92 msgid "Row #{0}: POS Invoice {1} has been {2}" -msgstr "" +msgstr "Red #{0}: Faktura Blagajne {1} je {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:73 msgid "Row #{0}: POS Invoice {1} is not against customer {2}" -msgstr "" +msgstr "Red #{0}: Faktura Blagajne {1} nije naspram klijenta {2}" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:88 msgid "Row #{0}: POS Invoice {1} is not submitted yet" -msgstr "" +msgstr "Red #{0}: Faktura Blagajne {1} još nije podnešena" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:123 msgid "Row #{0}: Party ID is required" -msgstr "" +msgstr "Red #{0}: ID Stranke je obavezan" #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:80 msgid "Row #{0}: Please select Item Code in Assembly Items" @@ -46385,11 +46385,11 @@ msgstr "Red #{0}: Odaberi Kod Artikla u Artiklima Montaže" #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:340 msgid "Row #{0}: Please select a valid Quality Inspection with Item Code {1}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete sa Kodom Artikla {1}." #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:333 msgid "Row #{0}: Please select a valid Quality Inspection with Reference Type {1} and Reference Name {2}." -msgstr "" +msgstr "Red #{0}: Odaberi važeću Kontrolu Kvalitete s Tipom Reference {1} i Nazivom Reference {2}." #: erpnext/manufacturing/doctype/production_plan/services/sub_assembly.py:82 msgid "Row #{0}: Please select the BOM No in Assembly Items" @@ -46413,7 +46413,7 @@ msgstr "Red #{0}: Ažuriraj račun odloženih prihoda/troškova u redu artikla i #: erpnext/assets/doctype/asset/asset.py:413 msgid "Row #{0}: Please use a different Finance Book." -msgstr "" +msgstr "Red #{0}: Koristi drugi Finansijski Registar." #: erpnext/manufacturing/doctype/bom/bom.py:378 #, python-format @@ -46435,7 +46435,7 @@ msgstr "Red #{0}: Količina mora biti pozitivan broj" #: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:429 msgid "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Item {2} against Batch {3} in Warehouse {4}." -msgstr "" +msgstr "Red #{0}: Količina bi trebala biti manja ili jednaka Dostupnoj Količini za Rezervaciju (Stvarna količina - Rezervisana količina) {1} za artikal {2} naspram Šarže {3} u Skladištu {4}." #: erpnext/stock/services/quality_inspection_service.py:77 msgid "Row #{0}: Quality Inspection is required for Item {1}" @@ -46512,7 +46512,10 @@ msgid "Row #{0}: Selling rate for item {1} is lower than its {2}.\n" "\t\t\t\t\tSelling {3} should be at least {4}.

Alternatively,\n" "\t\t\t\t\tyou can disable '{5}' in {6} to bypass\n" "\t\t\t\t\tthis validation." -msgstr "" +msgstr "Red #{0}: Prodajna cijena za artikal {1} je niža od njegove {2}.\n" +"\t\t\t\t\tProdaja {3} treba biti barem {4}.

Alternativno,\n" +"\t\t\t\t\tmožete onemogućiti '{5}' u {6} kako biste zaobišli\n" +"\t\t\t\t\tovu validaciju." #: erpnext/manufacturing/doctype/work_order/work_order.py:348 msgid "Row #{0}: Sequence ID must be {1} or {2} for Operation {3}." @@ -46520,7 +46523,7 @@ msgstr "Red #{0}: ID Sekvence mora biti {1} ili {2} za Operaciju {3}." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:528 msgid "Row #{0}: Serial No {1} cannot be returned since it was not transacted in original invoice {2}" -msgstr "" +msgstr "Red #{0}: Serijski Broj {1} ne može se vratiti jer nije naveden u originalnoj fakturi {2}" #: erpnext/stock/services/serial_batch_bundle_service.py:123 msgid "Row #{0}: Serial No {1} does not belong to Batch {2}" @@ -46637,11 +46640,11 @@ msgstr "Red #{0}: Šarža {1} je već istekla." #: erpnext/stock/doctype/stock_entry/stock_entry.py:408 msgid "Row #{0}: The job card item reference is missing. Kindly create the stock entry from the job card. If you have added the row manually then you won't be able to add job card item reference." -msgstr "" +msgstr "Red #{0}: Nedostaje referenca artikla na radnoj kartici. Stvori unos zaliha iz radne kartice. Ako ste red dodali ručno, nećete moći dodati referencu artikla na radnu karticu." #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:103 msgid "Row #{0}: The original Invoice {1} of return invoice {2} is not consolidated." -msgstr "" +msgstr "Red #{0}: Izvorna faktura {1} povratne fakture {2} nije konsolidirana." #: erpnext/stock/doctype/item/item.py:599 msgid "Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2}" @@ -46649,7 +46652,7 @@ msgstr "Red #{0}: Skladište {1} nije podređeno skladište grupnog skladišta { #: erpnext/manufacturing/doctype/workstation/workstation.py:190 msgid "Row #{0}: Timings conflict with row {1}" -msgstr "" +msgstr "Red #{0}: Vremenski sukob s redom {1}" #: erpnext/assets/doctype/asset/asset.py:656 msgid "Row #{0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" @@ -46673,7 +46676,7 @@ msgstr "Red #{0}: Radni Nalog postoji za punu ili djelomičnu količinu artiikla #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:500 msgid "Row #{0}: You cannot add positive quantities in a return invoice. Please remove item {1} to complete the return." -msgstr "" +msgstr "Redak #{0}: Ne možete dodati pozitivne količine u povratnu fakturu. Ukloni artikal {1} kako biste dovršili povrat." #: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:110 msgid "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." @@ -46685,7 +46688,7 @@ msgstr "Red #{0}: Odaberi Imovinu za Artikal {1}." #: erpnext/stock/doctype/pick_list/pick_list.py:235 msgid "Row #{0}: item {1} has been picked already." -msgstr "" +msgstr "Red #{0}: artikal {1} je već odabran." #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:142 #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:207 @@ -46694,7 +46697,7 @@ msgstr "Red #{0}: {1}" #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:142 msgid "Row #{0}: {1} account is not of type {2}" -msgstr "" +msgstr "Red #{0}: {1} račun nije tipa {2}" #: erpnext/public/js/controllers/buying.js:261 msgid "Row #{0}: {1} can not be negative for item {2}" @@ -46714,11 +46717,11 @@ msgstr "Red #{0}: {1} od {2} bi trebao biti {3}. Ažuriraj {1} ili odaberi drugi #: erpnext/stock/doctype/item/item.py:1511 msgid "Row #{0}: {1} {2} does not belong to Company {3}. Please select valid {4}." -msgstr "" +msgstr "Red #{0}: {1} {2} ne pripada tvrtki {3}. Odaberi valjani {4}." #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:126 msgid "Row #{0}: {1} {2} does not exist." -msgstr "" +msgstr "Red #{0}: {1} {2} ne postoji." #: erpnext/accounts/services/child_item_update.py:251 msgid "Row #{0}:Quantity for Item {1} cannot be zero." @@ -46895,7 +46898,7 @@ msgstr "Red {0}: Od vremena i do vremena je obavezano." #: erpnext/manufacturing/doctype/job_card/job_card.py:355 msgid "Row {0}: From Time and To Time of {1} are overlapping with {2}" -msgstr "" +msgstr "Red {0}: Vrijeme od i Vrijeme do {1} preklapaju se s {2}" #: erpnext/projects/doctype/timesheet/timesheet.py:225 msgid "Row {0}: From Time and To Time of {1} is overlapping with {2}" @@ -46919,7 +46922,7 @@ msgstr "Red {0}: Nevažeća referenca {1}" #: erpnext/controllers/taxes_and_totals.py:134 msgid "Row {0}: Item Tax template for {1} updated as per validity and rate applied" -msgstr "" +msgstr "Red {0}: Predložak PDV-a na Artikal za {1} ažuriran je prema valjanosti i primijenjenoj stopi" #: erpnext/controllers/selling_controller.py:659 msgid "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" @@ -46983,7 +46986,7 @@ msgstr "Red {0}: Odaberi Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:214 msgid "Row {0}: Please select a valid BOM for Item {1}." -msgstr "" +msgstr "Red {0}: Odaberi valjanu Sastavnicu za artikal {1}." #: erpnext/controllers/subcontracting_controller.py:208 msgid "Row {0}: Please select an active BOM for Item {1}." @@ -47055,7 +47058,7 @@ msgstr "Red {0}: Cijeli iznos troška za račun {1} u {2} je već dodijeljen." #: erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py:103 msgid "Row {0}: The item {1}, quantity must be a positive number" -msgstr "" +msgstr "Red {0}: Artikal {1}, količina mora biti pozitivan broj" #: erpnext/accounts/services/taxes.py:269 msgid "Row {0}: The {3} Account {1} does not belong to the company {2}" @@ -47116,7 +47119,7 @@ msgstr "Red {0}: {1} {2} je povezan sa {3}. Odaberi dokument koji pripada {4}." #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:132 msgid "Row {0}: {1} {2} must be submitted" -msgstr "" +msgstr "Red {0}: {1} {2} mora biti podnešen" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:111 msgid "Row {0}: {2} Item {1} does not exist in {2} {3}" @@ -47162,7 +47165,7 @@ msgstr "Redovi: {0} imaju 'Unos Plaćanja' kao Tip Reference. Ovo ne treba posta #: erpnext/controllers/accounts_controller.py:276 msgid "Rows: {0} in {1} section are invalid. Reference Name should point to a valid Payment Entry or Journal Entry." -msgstr "" +msgstr "Redovi: {0} u {1} sekciji su nevažeći. Naziv reference treba da ukazuje na važeći Unos Plaćanja ili Nalog Knjiženja." #. Label of the rule_applied (Check) field in DocType 'Pricing Rule Detail' #: erpnext/accounts/doctype/pricing_rule_detail/pricing_rule_detail.json @@ -47589,7 +47592,7 @@ msgstr "Prodajna Faktura nije podnešena" #: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:195 msgid "Sales Invoice isn't created by user {0}" -msgstr "" +msgstr "Prodajna Faktura nije izrađena od korisnika {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:472 msgid "Sales Invoice mode is activated in POS. Please create Sales Invoice instead." @@ -49055,7 +49058,7 @@ msgstr "Odabrani dokument mora biti u podnešenom stanju" #: erpnext/assets/doctype/asset/asset.py:1195 msgid "Selected {0} does not contain the Item Code {1}" -msgstr "" +msgstr "Odabrani {0} ne sadrži Kod Artikla {1}" #. Option for the 'Pickup Type' (Select) field in DocType 'Shipment' #: erpnext/stock/doctype/shipment/shipment.json @@ -49393,7 +49396,7 @@ msgstr "Serijski broj je već dodijeljen" #: erpnext/assets/doctype/asset_repair/asset_repair.py:296 msgid "Serial No Bundle is mandatory for Item {0}" -msgstr "" +msgstr "Paket Serijskih Brojeva je obavezan za artikal {0}" #: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py:33 msgid "Serial No Count" @@ -49458,7 +49461,7 @@ msgstr "Serijski Broj i Šarža" #: erpnext/stock/doctype/stock_settings/stock_settings.js:93 msgid "Serial No and Batch Selector cannot be used when Use Serial / Batch Fields is enabled." -msgstr "" +msgstr "Serijski Broj i birač Šarže ne mogu se koristiti kada je omogućeno Koristi Serijski Broj / Šaržu." #. Name of a report #. Label of a Link in the Stock Workspace @@ -49501,7 +49504,7 @@ msgstr "Serijski Broj {0} ne postoji" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:378 msgid "Serial No {0} is already Delivered. You cannot use it again in Manufacture / Repack entry." -msgstr "" +msgstr "Serijski broj {0} je već dostavljen. Ne možete ga ponovno koristiti u unosu Proizvodnje / Ponovnog pakiranja." #: erpnext/public/js/utils/barcode_scanner.js:435 msgid "Serial No {0} is already added" @@ -49517,11 +49520,11 @@ msgstr "Serijski broj {0} nije u {1} {2}, i ne može se vratiti naspram {1} {2}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:343 msgid "Serial No {0} is under maintenance contract until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod ugovorom o održavanju do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:336 msgid "Serial No {0} is under warranty until {1}" -msgstr "" +msgstr "Serijski Broj {0} je pod jamstvom do {1}" #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:322 msgid "Serial No {0} not found" @@ -49657,7 +49660,7 @@ msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mi #: erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py:298 msgid "Serial and Batch Bundle {0} should have voucher type as 'Maintenance Schedule'" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} treba imati tip verifikata kao 'Raspored Održavanja'" #. Label of the section_break_45 (Section Break) field in DocType #. 'Subcontracting Receipt Item' @@ -51136,7 +51139,7 @@ msgstr "Nedostaju neki obavezni podaci o tvrtki. Nemate dopuštenje za njihovo a #: erpnext/www/book_appointment/index.js:248 msgid "Something went wrong, please try again" -msgstr "" +msgstr "Nešto nije u redu, pokušajte ponovo" #: erpnext/accounts/doctype/pricing_rule/utils.py:754 msgid "Sorry, this coupon code is no longer valid" @@ -51387,7 +51390,7 @@ msgstr "Raspodijeli proviziju među više prodavača." #: erpnext/buying/doctype/purchase_order/purchase_order.js:600 #: erpnext/public/js/controllers/buying.js:558 msgid "Splitting {0} units of {1}" -msgstr "" +msgstr "Dijeljenje {0} jedinica od {1}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2195 msgid "Splitting {0} {1} into {2} rows as per Payment Terms" @@ -51509,15 +51512,15 @@ msgstr "Poredak" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:73 msgid "Standing scores must be continuous and cover 0 to 100 without gaps or overlaps" -msgstr "" +msgstr "Trenutni rezultati moraju biti kontinuirani i pokrivati od 0 do 100 bez praznina ili preklapanja" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:76 msgid "Standing scores must cover the full range from 0 to 100" -msgstr "" +msgstr "Trenutni rezultati moraju pokrivati cijeli raspon od 0 do 100" #: erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py:68 msgid "Standing {0} must have a minimum grade lower than its maximum grade" -msgstr "" +msgstr "{0} mora imati minimalnu ocjenu nižu od maksimalne ocjene" #: erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js:54 msgid "Start / Resume" @@ -51525,7 +51528,7 @@ msgstr "Pokreni / Nastavi" #: erpnext/accounts/doctype/accounting_period/accounting_period.py:45 msgid "Start Date cannot be after End Date" -msgstr "" +msgstr "Datum početka ne može biti nakon datuma završetka" #: erpnext/crm/doctype/email_campaign/email_campaign.py:40 msgid "Start Date cannot be before the current date" @@ -51591,7 +51594,7 @@ msgstr "Pokrenut je pozadinski zadatak za stvaranje {1} {0}. {2}" #: erpnext/public/js/bulk_transaction_processing.js:29 msgid "Starting a background job to create {0} {1}" -msgstr "" +msgstr "Pokretanje pozadinskog zadatka za stvaranje {0} {1}" #. Label of the date_dist_from_left_edge (Float) field in DocType 'Cheque Print #. Template' @@ -51802,7 +51805,7 @@ msgstr "Unos Zaključanih Zaliha {0} već postoji za odabrani vremenski raspon" #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry.py:100 msgid "Stock Closing Entry {0} has been queued for processing, the system will take some time to complete it." -msgstr "" +msgstr "Završni Unos Zaliha {0} je stavljen u red za obradu, sustavu će trebati neko vrijeme da ga dovrši." #: erpnext/stock/doctype/stock_closing_entry/stock_closing_entry_dashboard.py:9 msgid "Stock Closing Log" @@ -51885,11 +51888,11 @@ msgstr "Tip Unosa Zaliha" #: erpnext/stock/doctype/stock_entry_type/stock_entry_type.py:65 msgid "Stock Entry Type {0} cannot be set as standard" -msgstr "" +msgstr "Tip Unosa Zaliha {0} ne može se postaviti kao standard" #: erpnext/stock/doctype/pick_list/mapper.py:289 msgid "Stock Entry has already been created against this Pick List" -msgstr "" +msgstr "Unos Zaliha je već izrađen naspram ove Liste Odabira" #: erpnext/stock/doctype/batch/batch.js:138 msgid "Stock Entry {0} created" @@ -51897,7 +51900,7 @@ msgstr "Unos Zaliha {0} je kreiran" #: erpnext/manufacturing/doctype/job_card/job_card.py:1639 msgid "Stock Entry {0} has been created" -msgstr "" +msgstr "Unos Zaliha {0} je izrađen" #: erpnext/accounts/doctype/journal_entry/journal_entry.py:997 msgid "Stock Entry {0} is not submitted" @@ -52510,7 +52513,7 @@ msgstr "Zaliha nije dostupna za Artikal {0} u Skladištu {1}." #: erpnext/selling/page/point_of_sale/pos_controller.js:826 msgid "Stock quantity is not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}." -msgstr "" +msgstr "Količina na zalihi nije dovoljna za Artikal Kod: {0} u skladištu {1}. Dostupna količina {2} {3}." #: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:255 msgid "Stock transactions before {0} are frozen" @@ -53980,7 +53983,7 @@ msgstr "Ciljna Imovina {0} ne pripada tvrtki {1}" #: erpnext/assets/doctype/asset_capitalization/asset_capitalization.py:189 msgid "Target Asset {0} needs to be a composite asset" -msgstr "" +msgstr "Ciljana Imovina {0} mora biti složena imovina" #. Name of a DocType #: erpnext/setup/doctype/target_detail/target_detail.json @@ -55090,7 +55093,7 @@ msgstr "Tekst prikazan u financijskom izvješću (npr. 'Ukupni Prihod', 'Gotovin #: erpnext/stock/doctype/packing_slip/packing_slip.py:89 msgid "The 'From Package No.' field must not be empty or have a value less than 1." -msgstr "" +msgstr "\"Od Paketa Broj.\" polje ne smije biti prazno niti njegova vrijednost manja od 1." #. Description of the 'Current BOM' (Link) field in DocType 'BOM Update Tool' #: erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.json @@ -55099,7 +55102,7 @@ msgstr "Sastavnica koja će biti zamijenjena" #: erpnext/controllers/subcontracting_controller.py:1056 msgid "The Batch No {0} has not been supplied against the {1} {2}" -msgstr "" +msgstr "Broj Šarže {0} nije dostavljen naspram {1} {2}" #: erpnext/stock/serial_batch_bundle.py:1557 msgid "The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry." @@ -55107,7 +55110,7 @@ msgstr "Šarža {0} ima negativnu količinu {1}. Da biste to riješili, idite na #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1590 msgid "The Batch {0} of item {1} has negative stock in the warehouse {2}{3}. Please add a stock quantity of {4} to proceed with this entry. If it is not possible to make an adjustment entry, please enable 'Allow Negative Stock for Batch' in the batch {0} or in the Stock Settings to proceed. However, enabling this setting may lead to negative stock in the system. So please ensure the stock levels are adjusted as soon as possible to maintain the correct valuation rate." -msgstr "" +msgstr "Šarža {0} artikla {1} ima negativne zalihe u skladištu {2}{3}. Dodaj količinu zaliha od {4} da biste nastavili s ovim unosom. Ako nije moguće izvršiti unos prilagođavanja, omogućite 'Dozvoli Negativne Zalihe za Šaržu' za Šaržu {0} ili u Postavkama Zaliha da biste nastavili. Međutim, omogućavanje ove postavke može dovesti do negativnih zaliha u sustavu. Stoga, molimo vas da osigurate da se razina zaliha što prije prilagode kako bi se održala ispravna stopa vrednovanja." #: erpnext/crm/doctype/email_campaign/email_campaign.py:71 msgid "The Campaign '{0}' already exists for the {1} '{2}'" @@ -55135,7 +55138,7 @@ msgstr "Knjigovodstveni Unosi će biti otkazani u pozadini, može potrajati neko #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1156 msgid "The Item {0} does not have Serial No or Batch No" -msgstr "" +msgstr "Artikal {0} nema Serijski niti Šaržni Broj" #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:179 msgid "The Loyalty Program isn't valid for the selected company" @@ -55155,11 +55158,11 @@ msgstr "Lista Odabira koja ima Unose Rezervacije Zaliha ne može se ažurirati. #: erpnext/stock/doctype/stock_entry/services/manufacturing.py:127 msgid "The Process Loss Qty has been reset as per the Job Card's Process Loss Qty" -msgstr "" +msgstr "Količina gubitaka procesa poništena je prema količini gubitaka procesa na radnoj kartici" #: erpnext/stock/doctype/stock_entry/stock_entry.py:1375 msgid "The Process Loss Qty has been reset as per the job card's Process Loss Qty" -msgstr "" +msgstr "Količina gubitaka procesa poništena je prema količini gubitaka procesa na radnoj kartici" #: erpnext/setup/doctype/sales_person/sales_person.py:102 msgid "The Sales Person is linked with {0}" @@ -55175,7 +55178,7 @@ msgstr "Serijski Broj {0} je rezervisan naspram {1} {2} i ne može se koristiti #: erpnext/controllers/subcontracting_controller.py:1071 msgid "The Serial Nos {0} have not been supplied against the {1} {2}" -msgstr "" +msgstr "Serijski Brojevi {0} nisu dostavljeni naspram {1} {2}" #: erpnext/stock/doctype/stock_entry/stock_entry.py:950 msgid "The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0}" @@ -55231,7 +55234,7 @@ msgstr "Završena količina {0} operacije {1} ne može biti veća od završene k #: erpnext/accounts/doctype/dunning/dunning.py:87 msgid "The currency of invoice {0} ({1}) is different from the currency of this dunning ({2})." -msgstr "" +msgstr "Valuta Fakture {0} ({1}) razlikuje se od valute ove opomene ({2})." #: erpnext/selling/page/point_of_sale/pos_controller.js:200 msgid "The current POS opening entry is outdated. Please close it and create a new one." @@ -55284,7 +55287,7 @@ msgstr "Polje {0} u redu {1} nije postavljeno" #: erpnext/stock/stock_ledger.py:369 msgid "The field {0} is required for reposting" -msgstr "" +msgstr "Polje {0} je obavezno za ponovno knjiženje" #: erpnext/accounts/doctype/share_transfer/share_transfer.py:188 msgid "The fields From Shareholder and To Shareholder cannot be blank" @@ -55309,7 +55312,7 @@ msgstr "Brojevi Folija nisu usklađeni" #: erpnext/stock/doctype/putaway_rule/putaway_rule.py:306 msgid "The following Items, having Putaway Rules, could not be accommodated:" -msgstr "" +msgstr "Sljedeći artikli, koji imaju Pravila Odlaganja na Stranu, nisu mogli biti primjenjene:" #: erpnext/assets/doctype/asset_repair/asset_repair.py:137 msgid "The following Purchase Invoices are not submitted:" @@ -55337,7 +55340,7 @@ msgstr "Sljedeće osoblje još uvijek podnosi izvješća {0}:" #: erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py:185 msgid "The following invalid Pricing Rules are deleted:{0}" -msgstr "" +msgstr "Sljedeća nevažeća pravila određivanja cijena se brišu:{0}" #: erpnext/accounts/doctype/payment_request/payment_request.py:782 msgid "The following payment schedule(s) already exist:\n" @@ -55386,7 +55389,7 @@ msgstr "Artikli {items} nisu označeni kao {type_of} artikli. Možete ih omoguć #: erpnext/manufacturing/doctype/workstation/workstation.py:595 msgid "The job card {0} is in {1} state and you cannot complete it." -msgstr "" +msgstr "Radna Kartica {0} je u {1} stanju i ne možete je dovršiti." #: erpnext/manufacturing/doctype/workstation/workstation.py:589 msgid "The job card {0} is in {1} state and you cannot start it again." @@ -55424,11 +55427,11 @@ msgstr "Početno stanje možda ne odgovara vašem bankovnom izvodu. Želite li i #: erpnext/manufacturing/doctype/operation/operation.py:43 msgid "The operation {0} cannot be added multiple times" -msgstr "" +msgstr "Operacija {0} ne može se dodati više puta" #: erpnext/manufacturing/doctype/operation/operation.py:48 msgid "The operation {0} cannot be its own sub-operation" -msgstr "" +msgstr "Operacija {0} ne može biti vlastita podoperacija" #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:107 msgid "The original invoice should be consolidated before or along with the return invoice." @@ -55478,7 +55481,7 @@ msgstr "Procenat kojim vam je dozvoljeno prenijeti više naspram naručene koli #: erpnext/stock/doctype/item_price/item_price.py:71 msgid "The price list {0} does not exist or is disabled" -msgstr "" +msgstr "Cjenik {0} ne postoji ili je onemogućen" #. Description of the 'Last Purchase Rate' (Float) field in DocType 'Item' #: erpnext/stock/doctype/item/item.json @@ -55507,7 +55510,7 @@ msgstr "Odabrane Sastavnice nisu za istu artikal" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:544 msgid "The selected change account {0} does not belong to Company {1}." -msgstr "" +msgstr "Odabrani račun povrata {0} ne pripada {1}." #: erpnext/stock/doctype/batch/batch.py:157 msgid "The selected item cannot have Batch" @@ -55524,7 +55527,7 @@ msgstr "Prodavač i Kupac ne mogu biti isti" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:186 #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:198 msgid "The serial and batch bundle {0} is not linked to {1} {2}" -msgstr "" +msgstr "Serijski i Šaržni Paket {0} nije povezan sa {1} {2}" #: erpnext/stock/doctype/batch/batch.py:386 msgid "The serial no {0} does not belong to item {1}" @@ -55578,7 +55581,7 @@ msgstr "Zadatak je stavljen u red kao pozadinski posao. U slučaju da postoji bi #: erpnext/stock/doctype/material_request/material_request.py:352 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}" -msgstr "" +msgstr "Ukupna količina izdavanja / prijenosa {0} u Materijalnom Nalogu {1} ne može biti veća od dopuštene tražene količine {2} za artikal {3}" #: erpnext/stock/doctype/material_request/material_request.py:359 msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" @@ -55658,7 +55661,7 @@ msgstr "{0} {1} se ne poklapa s {0} {2} u {3} {4}" #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1730 msgid "The {0} {1} is in submitted state, please cancel it first" -msgstr "" +msgstr "{0} {1} je u podnešenom stanju, prvo ga otkažite" #: erpnext/manufacturing/doctype/job_card/job_card.py:1075 msgid "The {0} {1} is used to calculate the valuation cost for the finished good {2}." @@ -55699,7 +55702,7 @@ msgstr "U sustavu nema unosa kod kojih je datum odobravanja prije datuma knjiže #: erpnext/stock/report/item_variant_details/item_variant_details.py:25 msgid "There are no item variants for the selected item" -msgstr "" +msgstr "Nema varijanti artikla za odabrani artikal" #: erpnext/www/book_appointment/index.js:95 msgid "There are no slots available on this date" @@ -55747,7 +55750,7 @@ msgstr "Postoji jedna neusklađena transakcija prije {0}." #: erpnext/stock/doctype/stock_entry/stock_entry.py:887 msgid "There must be at least 1 Finished Good in this Stock Entry" -msgstr "" +msgstr "U ovom unosu zaliha mora biti barem jedan gotov proizvod" #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:152 msgid "There was an error creating Bank Account while linking with Plaid." @@ -55759,7 +55762,7 @@ msgstr "Došlo je do greške pri sinhronizaciji transakcija." #: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:174 msgid "There was an error updating Bank Account {0} while linking with Plaid." -msgstr "" +msgstr "Došlo je do pogreške prilikom ažuriranja bankovnog računa {0} prilikom povezivanja s Plaidom." #: banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx:81 msgid "There was an error while importing the bank statement." @@ -55815,7 +55818,7 @@ msgstr "Ovaj unos plaćanja usklađen je s {0}. Otkazivanje će ga automatski po #: erpnext/selling/doctype/product_bundle/product_bundle.py:121 msgid "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" -msgstr "" +msgstr "Ovaj Artikal Paket je povezan sa {0}. Morat ćete otkazati ove dokumente kako biste izbrisali ovaj Artikal Paket" #: erpnext/buying/doctype/purchase_order/mapper.py:251 msgid "This Purchase Order has been fully subcontracted." @@ -56156,7 +56159,7 @@ msgstr "Ovo će ograničiti pristup korisnika drugim zapisima zaposlenih" #: erpnext/controllers/selling_controller.py:901 msgid "This {0} will be treated as material transfer." -msgstr "" +msgstr "Ovo {0} će se tretirati kao prijenos materijala." #. Option for the 'Under Withheld Reason' (Select) field in DocType 'Tax #. Withholding Entry' @@ -56288,7 +56291,7 @@ msgstr "Vremenska Linija" #. 'Accounts Settings' #: erpnext/accounts/doctype/accounts_settings/accounts_settings.json msgid "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher" -msgstr "" +msgstr "Vremensko ograničenje (u sekundama) za svaki pozadinski zadatak stavljen u red čekanja prema verifikatu za zatvaranje knjigovodstvenog razdoblja" #: erpnext/manufacturing/doctype/workstation/workstation_job_card.html:36 #: erpnext/public/js/projects/timer.js:5 @@ -56577,7 +56580,7 @@ msgstr "Do Vremena" #: erpnext/projects/doctype/timesheet_detail/timesheet_detail.py:108 msgid "To Time cannot be before From Time" -msgstr "" +msgstr "Do Vremena ne može biti prije Od Vremena" #. Description of the 'Referral Code' (Data) field in DocType 'Sales Partner' #: erpnext/setup/doctype/sales_partner/sales_partner.json @@ -56633,7 +56636,7 @@ msgstr "Dostava Klijentu" #: erpnext/accounts/doctype/sales_invoice/services/pos.py:276 msgid "To cancel a {0} you need to cancel the POS Closing Entry {1}." -msgstr "" +msgstr "Za otkazivanje {0} morate otkazati unos zatvaranja Blagajne {1}." #: erpnext/accounts/doctype/sales_invoice/services/pos.py:290 msgid "To cancel this Sales Invoice you need to cancel the POS Closing Entry {0}." @@ -56645,7 +56648,7 @@ msgstr "Za kreiranje Zahtjeva Plaćanja obavezan je referentni dokument" #: erpnext/assets/doctype/asset_category/asset_category.py:120 msgid "To enable Capital Work in Progress Accounting, you must select Capital Work in Progress Account in accounts table" -msgstr "" +msgstr "Da biste omogućili knjigovodstvo nedovršenih kapitalnih radova, morate odabrati Račun nedovršenih kapitalnih radova u tablici računa" #: erpnext/manufacturing/doctype/production_plan/production_plan.js:734 msgid "To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked." @@ -56824,19 +56827,19 @@ msgstr "Ukupni Predujam" #: erpnext/public/js/utils.js:250 msgid "Total Advance Paid" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed" #: erpnext/public/js/utils.js:195 msgid "Total Advance Paid: {0}" -msgstr "" +msgstr "Ukupno Plaćeno Unaprijed: {0}" #: erpnext/public/js/utils.js:252 msgid "Total Advance Received" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed" #: erpnext/public/js/utils.js:198 msgid "Total Advance Received: {0}" -msgstr "" +msgstr "Ukupno Primljeno Unaprijed: {0}" #. Label of the total_allocated_amount (Currency) field in DocType 'Payment #. Entry' @@ -57495,7 +57498,7 @@ msgstr "Ukupno Vrijeme u minutama" #: erpnext/public/js/utils.js:253 msgid "Total Unpaid" -msgstr "" +msgstr "Ukupno Neplaćeno" #: erpnext/public/js/utils.js:200 msgid "Total Unpaid: {0}" @@ -57595,7 +57598,7 @@ msgstr "Ukupno sati: {0}" #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:574 #: erpnext/accounts/doctype/sales_invoice/services/pos.py:190 msgid "Total payments amount can't be greater than {0}" -msgstr "" +msgstr "Ukupni iznos plaćanja ne može biti veći od {0}" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:66 msgid "Total percentage against cost centers should be 100" @@ -57614,7 +57617,7 @@ msgstr "Ukupno {0} ({1})" #: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:248 msgid "Total {0} for all items is zero, maybe you should change 'Distribute Charges Based On'" -msgstr "" +msgstr "Ukupno {0} za sve artikle je nula, možda biste trebali promijeniti 'Raspodjeli Naknade na Temelju'" #: erpnext/controllers/trends.py:25 erpnext/controllers/trends.py:32 msgid "Total(Amt)" @@ -58155,7 +58158,7 @@ msgstr "Probna Bilanca Stranke" #: erpnext/accounts/report/trial_balance/trial_balance.py:595 msgid "Trial Balance requires {0} to be synced to DuckDB" -msgstr "" +msgstr "Probna Bilanca zahtijeva sinhronizaciju {0} sa DuckDB-om" #. Label of the trial_period_end (Date) field in DocType 'Subscription' #: erpnext/accounts/doctype/subscription/subscription.json @@ -59399,7 +59402,7 @@ msgstr "Korisnik nije primijenio pravilo na fakturi {0}" #: erpnext/crm/frappe_crm_api.py:175 msgid "User not allowed to synchronize data from Frappe CRM on ERPNext. Contact System Manager of ERPNext." -msgstr "" +msgstr "Korisniku nije dopuštena sinkronizacija podataka iz Prodajne Podrške u Sustav. Obratite se Upravitelju Sustava." #: erpnext/setup/doctype/employee/employee.py:298 msgid "User {0} does not exist" @@ -59415,7 +59418,7 @@ msgstr "Korisnik {0} je već dodijeljen {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:62 msgid "User {0} is disabled. Please select valid user/cashier" -msgstr "" +msgstr "Korisnik {0} je onemogućen. Odaberi valjanog korisnika/blagajnika" #: erpnext/setup/doctype/employee/employee.py:365 msgid "User {0}: Removed Employee Self Service role as there is no mapped employee." @@ -59763,7 +59766,7 @@ msgstr "Naknade za tip vrijednovanja ne mogu biti označene kao Inkluzivne" #: erpnext/public/js/controllers/accounts.js:228 msgid "Valuation type charges cannot be marked as Inclusive" -msgstr "" +msgstr "Naknade tipa procjene vrijednosti ne mogu biti označene kao uključene" #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js:58 msgid "Value (G - D)" @@ -61329,11 +61332,11 @@ msgstr "Sažetka Izvješća Radnog Naloga" #: erpnext/stock/doctype/material_request/material_request.py:579 msgid "Work Order cannot be created for the following reason:
{0}" -msgstr "" +msgstr "Radni Nalog se ne može izraditi iz sljedećeg razloga:
{0}" #: erpnext/manufacturing/doctype/work_order/work_order.py:857 msgid "Work Order cannot be raised against an Item Template" -msgstr "" +msgstr "Radni Nalog ne može se pokrenuti na temelju Predloška Artikla" #: erpnext/manufacturing/doctype/work_order/work_order.py:1123 #: erpnext/manufacturing/doctype/work_order/work_order.py:1170 @@ -61686,7 +61689,7 @@ msgstr "Uvoziš podatke za Listu Koda:" #: erpnext/accounts/services/child_item_update.py:232 msgid "You are not allowed to update as per the conditions set in {0} Workflow." -msgstr "" +msgstr "Nije vam dopušteno ažuriranje prema uvjetima postavljenim u {0} Radnom Tijeku." #: erpnext/accounts/services/gl_validator.py:114 msgid "You are not authorized to add or update entries before {0}" @@ -61706,7 +61709,7 @@ msgstr "Birate više od potrebne količine za artikal {0}. Provjerite postoji li #: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:111 msgid "You can add the original invoice {0} manually to proceed." -msgstr "" +msgstr "Izvornu Fakturu {0} možete dodati ručno da biste nastavili." #: banking/src/components/features/BankReconciliation/Rules/RuleForm.tsx:743 msgid "You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25)." @@ -61718,7 +61721,7 @@ msgstr "Takođe možete kopirati i zalijepiti ovu vezu u svoj pretraživač" #: erpnext/assets/doctype/asset_category/asset_category.py:124 msgid "You can also set default CWIP account in Company {0}" -msgstr "" +msgstr "Također možete postaviti standard Račun Kapitalnog Posla u Toku u {0}" #: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:761 msgid "You can change the parent account to a Balance Sheet account or select a different account." @@ -61747,7 +61750,7 @@ msgstr "Možete odabrati samo jedan način plaćanja kao standard" #: erpnext/selling/page/point_of_sale/pos_payment.js:595 msgid "You can redeem up to {0}." -msgstr "" +msgstr "Možete iskoristiti do {0}." #: banking/src/components/features/BankReconciliation/IncorrectlyClearedEntries.tsx:193 msgid "You can reset the clearing dates of these entries here." @@ -61779,11 +61782,11 @@ msgstr "Ne možete kreirati {0} unutar zatvorenog Knjigovodstvenog Perioda {1}" #: erpnext/accounts/services/gl_validator.py:64 msgid "You cannot create or cancel any accounting entries within the closed Accounting Period {0}" -msgstr "" +msgstr "Ne možete izraditi niti otkazati nikakve knjigovodstvene zapise unutar zatvorenog knjigovodstvenog perioda. {0}" #: erpnext/accounts/services/gl_validator.py:145 msgid "You cannot create/amend any accounting entries until this date." -msgstr "" +msgstr "Ne možete izraditi/izmijeniti bilo koje knjigovodstvene unose do ovog datuma." #: erpnext/accounts/doctype/journal_entry/journal_entry.py:675 msgid "You cannot credit and debit same account at the same time" @@ -61795,7 +61798,7 @@ msgstr "Ne možete izbrisati tip projekta 'Eksterni'" #: erpnext/setup/doctype/department/department.js:19 msgid "You cannot edit the root node." -msgstr "" +msgstr "Ne možete uređivati korijenski čvor." #: erpnext/accounts/doctype/accounts_settings/accounts_settings.py:198 msgid "You cannot enable both the settings '{0}' and '{1}'." @@ -61803,15 +61806,15 @@ msgstr "Ne možete omogućiti i '{0}' i '{1} postavke." #: erpnext/manufacturing/doctype/job_card/job_card.py:1441 msgid "You cannot make any changes to Job Card since Work Order is closed." -msgstr "" +msgstr "Ne možete unositi nikakve promjene na Radnoj Kartici jer je Radni Nalog zatvoren." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:167 msgid "You cannot outward the following {0} as they are either Delivered, Inactive or located in a different warehouse." -msgstr "" +msgstr "Ne možete poslati sljedeće {0} jer su ili Isporučeno, Neaktivno ili se nalaze u drugom skladištu." #: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:229 msgid "You cannot process the serial number {0} as it has already been used in the SABB {1}. {2} If you want to inward the same serial number multiple times, then enable 'Allow existing Serial No to be Manufactured/Received again' in the {3}" -msgstr "" +msgstr "Ne možete obraditi serijski broj {0} jer je već korišten u Serijskom i Šaržnom Paketu {1}. {2} ako želite da primite isti serijski broj više puta, tada omogućite 'Dozvoli da se postojeći Serijski Broj ponovo Proizvede/Primi' u {3}" #: erpnext/selling/page/point_of_sale/pos_payment.js:625 msgid "You cannot redeem more than {0}." @@ -61819,7 +61822,7 @@ msgstr "Ne možete iskoristiti više od {0}." #: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:212 msgid "You cannot repost item valuation before {0}" -msgstr "" +msgstr "Ne možete ponovo knjižiti procjenu vrijednosti artikla prije {0}" #: erpnext/accounts/doctype/subscription/subscription.py:832 msgid "You cannot restart a Subscription that is not cancelled." @@ -61827,7 +61830,7 @@ msgstr "Ne možete ponovo pokrenuti Pretplatu koja nije otkazana." #: erpnext/selling/page/point_of_sale/pos_payment.js:281 msgid "You cannot submit an empty order." -msgstr "" +msgstr "Ne možete podnijeti prazan nalog." #: erpnext/selling/page/point_of_sale/pos_payment.js:280 msgid "You cannot submit the order without payment." @@ -61843,7 +61846,7 @@ msgstr "Ne možete {0} ovaj dokument jer postoji drugi Unos Zatvaranje Perioda { #: erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py:165 msgid "You do not have enough permission to access {0}: {1}" -msgstr "" +msgstr "Nemate dovoljno dopuštenja za pristup {0}: {1}" #: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:82 msgid "You do not have permission to import and submit bank transactions" @@ -61856,7 +61859,7 @@ msgstr "Nemate dopuštenje za uvoz bankovnih transakcija" #: erpnext/accounts/services/child_item_update.py:210 msgid "You do not have permissions to {0} items in a {1}." -msgstr "" +msgstr "Nemate dopuštenja za {0} artikala u {1}." #: erpnext/accounts/doctype/loyalty_program/loyalty_program.py:187 msgid "You don't have enough Loyalty Points to redeem" @@ -61884,7 +61887,7 @@ msgstr "Nemate dopuštenje za ažuriranje ovog dokumenta. Obratite se Upravitelj #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py:310 msgid "You had {0} errors while creating opening invoices. Check {1} for more details" -msgstr "" +msgstr "Imali ste {0} pogrešaka prilikom izrade početnih računa. Pogledajte {1} za više detalja" #: erpnext/public/js/utils.js:1055 msgid "You have already selected items from {0} {1}" @@ -61904,7 +61907,7 @@ msgstr "Omogućili ste {0} i {1} u {2}. To može dovesti do umetanja cijena iz z #: erpnext/stock/doctype/shipment/shipment.js:442 msgid "You have entered a duplicate Delivery Note on row {0}. Please rectify and try again." -msgstr "" +msgstr "Unijeli ste duplikat Dostavnice u red {0}. Ispravi grešku i pokušaj ponovo." #: banking/src/components/features/BankReconciliation/BankPicker.tsx:64 msgid "You have not added any bank accounts to your company." @@ -61928,7 +61931,7 @@ msgstr "Morate odabrati Klijenta prije dodavanja Artikla." #: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:282 msgid "You need to cancel POS Closing Entry {0} to be able to cancel this document." -msgstr "" +msgstr "Morate otkazati Unos Zatvaranje Blagajne {0} da biste mogli otkazati ovaj dokument." #: erpnext/accounts/services/taxes.py:277 msgid "You selected the account group {1} as {2} Account in row {0}. Please select a single account." @@ -61984,7 +61987,7 @@ msgstr "Nulto Stanje" #: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353 msgid "Zero Balance Journal: {0}" -msgstr "" +msgstr "Žurnal Nultog Stanja: {0}" #: erpnext/regional/report/uae_vat_201/uae_vat_201.py:78 msgid "Zero Rated" @@ -62108,7 +62111,7 @@ msgstr "naziv polja" #: erpnext/setup/doctype/item_group/item_group.py:49 msgid "for tax category {0}" -msgstr "" +msgstr "za PDV kategoriju {0}" #. Option for the 'Service Provider' (Select) field in DocType 'Currency #. Exchange Settings' @@ -62467,7 +62470,7 @@ msgstr "{0} ne može biti negativan" #: erpnext/accounts/doctype/sales_invoice/services/loyalty.py:77 msgid "{0} cannot be cancelled since the Loyalty Points earned has been redeemed. First cancel the {1} No {2}" -msgstr "" +msgstr "{0} se ne može otkazati jer su osvojeni bodovi vjernosti iskorišteni. Prvo otkažite {1} Ne {2}" #: erpnext/accounts/doctype/pos_settings/pos_settings.py:53 msgid "{0} cannot be changed with opened Opening Entries." @@ -62475,7 +62478,7 @@ msgstr "{0} se ne može mijenjati s otvorenim Početnim Unosima." #: erpnext/public/js/utils/sales_common.js:336 msgid "{0} cannot be greater than 100" -msgstr "" +msgstr "{0} ne može biti veće od 100" #: erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py:136 msgid "{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}" @@ -62544,7 +62547,7 @@ msgstr "{0} je uspješno podnešen" #: erpnext/controllers/buying_controller.py:289 msgid "{0} has submitted assets linked to it. You need to cancel the assets to create purchase return." -msgstr "" +msgstr "{0} je podnio/la imovinu povezanu s njim/njom. Morate otkazati imovinu da biste izradili povrat." #: erpnext/projects/doctype/project/project_dashboard.html:15 msgid "{0} hours" @@ -62556,7 +62559,7 @@ msgstr "{0} u redu {1}" #: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py:66 msgid "{0} is a child company." -msgstr "" +msgstr "{0} je podređena tvrtka." #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:465 msgid "{0} is a child table and will be deleted automatically with its parent" @@ -62639,7 +62642,7 @@ msgstr "{0} nije omogućen u {1}" #: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:649 msgid "{0} is not running. Cannot trigger events for this document" -msgstr "" +msgstr "{0} se ne izvršava. Ne može pokrenuti događaje za ovaj dokument" #: erpnext/stock/doctype/material_request/material_request.py:478 msgid "{0} is not the default supplier for any items." @@ -62647,7 +62650,7 @@ msgstr "{0} nije standard dobavljač za bilo koji artikal." #: erpnext/accounts/doctype/payment_entry/payment_entry.py:2686 msgid "{0} is on hold until {1}" -msgstr "" +msgstr "{0} je na čekanju do {1}" #: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:68 msgid "{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry." @@ -62822,11 +62825,11 @@ msgstr "{0} {1} je već povezan sa Zajedničkim Kodom {2}." #: erpnext/accounts/doctype/party_link/party_link.py:53 #: erpnext/accounts/doctype/party_link/party_link.py:63 msgid "{0} {1} is already linked with another {2}" -msgstr "" +msgstr "{0} {1} je već povezan s drugim {2}" #: erpnext/accounts/doctype/party_link/party_link.py:40 msgid "{0} {1} is already linked with {2} {3}" -msgstr "" +msgstr "{0} {1} je već povezan s {2} {3}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:711 msgid "{0} {1} is associated with {2}, but Party Account is {3}" @@ -62867,7 +62870,7 @@ msgstr "{0} {1} nije aktivan" #: erpnext/accounts/doctype/bank_transaction/bank_transaction.py:452 msgid "{0} {1} is not affecting bank account {2}" -msgstr "" +msgstr "{0} {1} ne utječe na bankovni račun {2}" #: erpnext/accounts/doctype/payment_entry/payment_entry.py:688 msgid "{0} {1} is not associated with {2} {3}" diff --git a/erpnext/locale/sv.po b/erpnext/locale/sv.po index 13dd28de655..5c7e61ce3b7 100644 --- a/erpnext/locale/sv.po +++ b/erpnext/locale/sv.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" "POT-Creation-Date: 2026-06-28 10:20+0000\n" -"PO-Revision-Date: 2026-06-29 20:08\n" +"PO-Revision-Date: 2026-07-01 20:39\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Swedish\n" "MIME-Version: 1.0\n" @@ -8227,7 +8227,7 @@ msgstr "Saldo Historik per Parti" #: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:183 #: erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py:86 msgid "Batchwise Valuation" -msgstr "Partivis Värdering" +msgstr "Partibaserad Värdering" #. Label of the section_break_3 (Section Break) field in DocType 'Stock #. Reconciliation Item' @@ -9319,7 +9319,7 @@ msgstr "Beräkna Uppskatade Ankomst Tider" #. Settings' #: erpnext/selling/doctype/selling_settings/selling_settings.json msgid "Calculate Product Bundle price based on child Item's rates" -msgstr "Beräkna Artikel Paket pris baserat på priser för underordnade artiklar" +msgstr "Beräkna Artikel Paket pris baserat på priser för paket artiklar" #. Description of the 'Hidden Line (Internal Use Only)' (Check) field in #. DocType 'Financial Report Row' @@ -15680,7 +15680,7 @@ msgstr "Avdraget från" #. Deduction Certificate' #: erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json msgid "Deductee Details" -msgstr "Avdragsberättigad Detaljer" +msgstr "Avdragstagare Detaljer" #. Label of a Workspace Sidebar Item #: erpnext/workspace_sidebar/taxes.json @@ -17908,7 +17908,7 @@ msgstr "Uppdatera inte Varianter vid Spara" #. Settings' #: erpnext/stock/doctype/stock_settings/stock_settings.json msgid "Do not use Batch-wise Valuation" -msgstr "Använd inte Partivis Värdering" +msgstr "Använd inte Partibaserad Värdering" #: erpnext/assets/doctype/asset/asset.js:957 msgid "Do you really want to restore this scrapped asset?" @@ -19193,9 +19193,9 @@ msgid "Enabling this will do the following:\n" msgstr "Om du aktiverar detta kommer följande att hända:\n" "\n" -"Observera: Om detta är aktiverat kommer uppdatering av pris för artikel paket i artikel tabell inte att ändra dess pris. Det kommer att återställas till det pris som baseras på dess underordnade artiklar när dokumentet sparas." +"Observera: Om detta är aktiverat kommer uppdatering av pris för artikel paket i artikel tabell inte att ändra deras pris. Det kommer att återställas till pris som baseras på paket artiklar när dokument sparas." #. Label of the encashment_date (Date) field in DocType 'Employee' #: erpnext/setup/doctype/employee/employee.json @@ -33834,7 +33834,7 @@ msgstr "Öppning Faktura Verktyg" #: erpnext/accounts/doctype/purchase_invoice/services/gl_composer.py:832 #: erpnext/accounts/doctype/sales_invoice/services/gl_composer.py:642 msgid "Opening Invoice has rounding adjustment of {0}.

'{1}' account is required to post these values. Please set it in Company: {2}.

Or, '{3}' can be enabled to not post any rounding adjustment." -msgstr "Öppning Fakturan har avrundning justering på {0}.

'{1}' konto erfordras för att bokföra dessa värden. Ange det i Bolag: {2}.

Eller så kan '{3}' aktiveras för att inte bokföra någon avrundning justering." +msgstr "Öppning Faktura har avrundning justering på {0}.

'{1}' konto erfordras för att bokföra dessa värden. Ange det i Bolag: {2}.

Eller så kan '{3}' aktiveras för att inte bokföra någon avrundning justering." #: erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html:8 msgid "Opening Invoices" @@ -59194,7 +59194,7 @@ msgstr "Använd Python filter för att hämta Konton" #. Label of the use_batchwise_valuation (Check) field in DocType 'Batch' #: erpnext/stock/doctype/batch/batch.json msgid "Use Batch-wise Valuation" -msgstr "Använd Partivis Värdering" +msgstr "Använd Partibaserad Värdering" #. Label of the use_csv_sniffer (Check) field in DocType 'Bank Statement #. Import' @@ -60825,7 +60825,7 @@ msgstr "Garanti Utgång (Serienummer)" #: erpnext/stock/doctype/serial_no/serial_no.json #: erpnext/support/doctype/warranty_claim/warranty_claim.json msgid "Warranty Expiry Date" -msgstr "Garanti Utgångsdatum" +msgstr "Garanti Utgång Datum" #. Label of the warranty_period (Int) field in DocType 'Serial No' #: erpnext/stock/doctype/serial_no/serial_no.json diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index b16960c994e..e9edef76944 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -739,7 +739,7 @@ class BOM(WebsiteGenerator): ) ) - def check_recursion(self, bom_list=None): + def check_recursion(self): """Check whether recursion occurs in any bom""" bom_list = self.traverse_tree() child_items = frappe.get_all( @@ -861,21 +861,30 @@ class BOM(WebsiteGenerator): self.append("items", row) - def traverse_tree(self, bom_list=None): - count = 0 - if not bom_list: - bom_list = [] + def traverse_tree(self): + """Return this BOM and every descendant BOM. The whole sub-tree is fetched in one recursive + CTE (frappe.qb) instead of a query-per-node walk; the only caller (check_recursion) uses the + result purely as a membership set. Portable across postgres and mariadb 10.2+.""" + bom_item = frappe.qb.DocType("BOM Item") + tree = frappe.qb.Table("bom_tree") - if self.name not in bom_list: - bom_list.append(self.name) + seed = ( + frappe.qb.from_(bom_item) + .select(bom_item.bom_no.as_("bom")) + .where((bom_item.parent == self.name) & (bom_item.bom_no != "") & (bom_item.parenttype == "BOM")) + ) + recursion = ( + frappe.qb.from_(bom_item) + .join(tree) + .on(bom_item.parent == tree.bom) + .select(bom_item.bom_no) + .where((bom_item.bom_no != "") & (bom_item.parenttype == "BOM")) + ) + descendants = ( + frappe.qb.with_(seed + recursion, "bom_tree", recursive=True).from_(tree).select(tree.bom) + ).run(pluck=True) - while count < len(bom_list): - for child_bom in _get_bom_children(bom_list[count]): - if child_bom not in bom_list: - bom_list.append(child_bom) - count += 1 - bom_list.reverse() - return bom_list + return [self.name, *descendants] def company_currency(self): return erpnext.get_company_currency(self.company) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 7797d819e39..2af6dbecb1a 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -881,8 +881,13 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non warehouse_list = [warehouse_list] if not warehouse_list: + # Reconcile every warehouse the item has a non-zero balance in -- including + # negative balances left by other tests. `get_valuation_rate` averages + # Sum(stock_value)/Sum(actual_qty) across all bins, so a leftover negative + # balance in one warehouse can cancel the reset qty elsewhere and make the + # average collapse to 0, which is a source of flaky BOM-cost failures. warehouse_list = frappe.get_all( - "Bin", filters={"item_code": item_code, "actual_qty": [">", 0]}, pluck="warehouse" + "Bin", filters={"item_code": item_code, "actual_qty": ["!=", 0]}, pluck="warehouse" ) if not warehouse_list: diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index 853de3ea945..ebc064396bf 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -67,29 +67,33 @@ def update_cost_in_level(doc: "BOMUpdateLog", bom_list: list[str], batch_name: i frappe.db.commit() # nosemgrep -def get_ancestor_boms(new_bom: str, bom_list: list | None = None) -> list: - "Recursively get all ancestors of BOM." - - bom_list = bom_list or [] +def get_ancestor_boms(new_bom: str) -> list: + """Return every ancestor BOM of `new_bom` (BOMs that consume it, transitively) in one recursive + CTE built with frappe.qb -- portable across postgres and mariadb 10.2+. `UNION` makes it + cycle-safe (it stops once no new BOM is reached); a BOM that is its own ancestor is rejected.""" bom_item = frappe.qb.DocType("BOM Item") + tree = frappe.qb.Table("ancestor_boms") - parents = ( + seed = ( frappe.qb.from_(bom_item) - .select(bom_item.parent) + .select(bom_item.parent.as_("bom")) .where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) - .run(as_dict=True) ) + recursion = ( + frappe.qb.from_(bom_item) + .join(tree) + .on(bom_item.bom_no == tree.bom) + .select(bom_item.parent) + .where((bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) + ) + ancestors = ( + frappe.qb.with_(seed + recursion, "ancestor_boms", recursive=True).from_(tree).select(tree.bom) + ).run(pluck=True) - for d in parents: - if new_bom == d.parent: - frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) + if new_bom in ancestors: + frappe.throw(_("BOM recursion: {0} cannot be an ancestor of itself").format(new_bom)) - if d.parent not in tuple(bom_list): - bom_list.append(d.parent) - - get_ancestor_boms(d.parent, bom_list) - - return bom_list + return ancestors def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index e720bd96319..ac2b38ea216 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2322,6 +2322,145 @@ class TestProductionPlan(ERPNextTestSuite): self.assertEqual(len(reserved_entries), 0) frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def test_no_stock_reservation_via_purchase_receipt_when_reserve_stock_disabled(self): + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.mapper import make_purchase_order + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0) + + bom_tree = {"FG For SR No Auto Reserve": {"RM For SR No Auto Reserve": {}}} + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + # reserve_stock is deliberately left unset (defaults to 0): this is what happens when + # "Auto Reserve Stock" is off and nobody ticks "Reserve Stock" on the Production Plan by hand. + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=5, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + for_warehouse=warehouse, + ) + plan.get_sub_assembly_items() + plan.set("mr_items", []) + for d in get_items_for_material_requests(plan.as_dict()): + plan.append("mr_items", d) + plan.save() + + self.assertEqual(plan.reserve_stock, 0) + plan.submit() + + plan.submit_material_request = 1 + plan.make_material_request() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + self.assertGreater(len(material_requests), 0) + + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertEqual(len(reserved_entries), 0) + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_ignores_production_plans_with_reserve_stock_off_on_shared_purchase_order(self): + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0) + + warehouse = "_Test Warehouse - _TC" + + bom_reserve = create_nested_bom({"FG SR Mixed Reserve": {"RM SR Mixed Reserve": {}}}, prefix="") + bom_skip = create_nested_bom({"FG SR Mixed Skip": {"RM SR Mixed Skip": {}}}, prefix="") + + def make_submitted_plan(item_code, reserve_stock): + plan = create_production_plan( + item_code=item_code, + planned_qty=5, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=reserve_stock, + ) + plan.get_sub_assembly_items() + plan.set("mr_items", []) + for d in get_items_for_material_requests(plan.as_dict()): + plan.append("mr_items", d) + plan.save() + plan.submit() + plan.submit_material_request = 1 + plan.make_material_request() + return plan + + plan_reserve = make_submitted_plan(bom_reserve.item, reserve_stock=1) + plan_skip = make_submitted_plan(bom_skip.item, reserve_stock=0) + + self.assertEqual(plan_reserve.reserve_stock, 1) + self.assertEqual(plan_skip.reserve_stock, 0) + + mr_reserve = frappe.get_all( + "Material Request", filters={"production_plan": plan_reserve.name}, pluck="name" + )[0] + mr_skip = frappe.get_all( + "Material Request", filters={"production_plan": plan_skip.name}, pluck="name" + )[0] + + # One Purchase Order pulling rows from both Material Requests, so the Purchase Receipt made + # from it has both a reservable and a non-reservable Production Plan reference in `doc.items`. + po = frappe.new_doc("Purchase Order") + po.supplier = "_Test Supplier" + po.company = plan_reserve.company + po.schedule_date = nowdate() + + for mr_name in (mr_reserve, mr_skip): + mr = frappe.get_doc("Material Request", mr_name) + for item in mr.items: + po.append( + "items", + { + "item_code": item.item_code, + "qty": item.qty, + "rate": 100, + "schedule_date": nowdate(), + "warehouse": warehouse, + "material_request": mr.name, + "material_request_item": item.name, + }, + ) + + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + reserved_for_plan_reserve = StockReservation(plan_reserve).get_reserved_entries( + "Production Plan", plan_reserve.name + ) + reserved_for_plan_skip = StockReservation(plan_skip).get_reserved_entries( + "Production Plan", plan_skip.name + ) + + self.assertGreater(len(reserved_for_plan_reserve), 0) + self.assertEqual(len(reserved_for_plan_skip), 0) + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def test_stock_reservation_restored_on_work_order_cancel(self): # Spec #5 (cancellation path): when a Work Order created from a Production Plan is cancelled, # the reservation that was transferred PP -> WO must flow back to the still-open Production diff --git a/erpnext/manufacturing/doctype/work_order/services/required_items.py b/erpnext/manufacturing/doctype/work_order/services/required_items.py index a8a415ca4fc..c43d1a43e4f 100644 --- a/erpnext/manufacturing/doctype/work_order/services/required_items.py +++ b/erpnext/manufacturing/doctype/work_order/services/required_items.py @@ -149,6 +149,16 @@ class RequiredItemsService: self.recompute_material_transferred_for_manufacturing(transferred_items) + def refresh_material_transferred_for_manufacturing(self): + """Recompute material_transferred_for_manufacturing only, without touching per-row + transferred_qty or stock reservations. Used to get a status decision (Not Started vs + In Process) based on fresh data, ahead of the fuller update_required_items() pass. + """ + if self.doc.skip_transfer: + return + transferred_items = self._material_transfer_qty_by_item(is_return=0) + self.recompute_material_transferred_for_manufacturing(transferred_items) + def recompute_material_transferred_for_manufacturing(self, transferred_items): """Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty.""" # When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the diff --git a/erpnext/manufacturing/doctype/work_order/services/status.py b/erpnext/manufacturing/doctype/work_order/services/status.py index cb9e49a2cce..d22ee8bd937 100644 --- a/erpnext/manufacturing/doctype/work_order/services/status.py +++ b/erpnext/manufacturing/doctype/work_order/services/status.py @@ -87,6 +87,12 @@ class StatusService: def update_status(self, status=None): """Update status of work order if unknown""" + if self.doc.docstatus == 1: + # Refresh material_transferred_for_manufacturing before deciding status so pick-list- + # driven transfers (where this qty is derived from item transfers, not fg_completed_qty) + # are reflected immediately, instead of only after the next status update call. + self.doc.refresh_material_transferred_for_manufacturing() + if self.doc.status != "Closed": if status not in ["Stopped", "Closed"]: status = self.get_status(status) @@ -126,7 +132,9 @@ class StatusService: status = ( "In Process" - if flt(self.doc.material_transferred_for_manufacturing) > 0 or self.doc.skip_transfer + if flt(self.doc.material_transferred_for_manufacturing) > 0 + or self.doc.skip_transfer + or self._has_transferred_material() else "Not Started" ) precision = frappe.get_precision("Work Order", "produced_qty") @@ -135,6 +143,26 @@ class StatusService: status = "Completed" return status + def _has_transferred_material(self): + """True if any raw material was transferred against this work order via a pick list + (these leave material_transferred_for_manufacturing at 0 via the min-fraction rule).""" + ste = frappe.qb.DocType("Stock Entry") + ste_child = frappe.qb.DocType("Stock Entry Detail") + qty = ( + frappe.qb.from_(ste) + .inner_join(ste_child) + .on(ste_child.parent == ste.name) + .select(Sum(ste_child.transfer_qty)) + .where( + (ste.work_order == self.doc.name) + & (ste.docstatus == 1) + & (ste.purpose == "Material Transfer for Manufacture") + & (ste.is_return == 0) + & (ste.pick_list.isnotnull()) + ) + ).run()[0][0] + return flt(qty) > 0 + def _is_partial_skip_transfer(self): return bool( self.doc.skip_transfer diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 0a4bbcddd2a..a101bb04b4d 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1528,6 +1528,38 @@ class TestWorkOrder(ERPNextTestSuite): work_order.reload() self.assertEqual(work_order.material_transferred_for_manufacturing, 2.0) + def test_status_in_process_when_only_one_required_item_transferred(self): + """Stock Entry created from a Pick List that picked only one of the required items: + min-fraction keeps material_transferred_for_manufacturing at 0, but the work order must + still move to In Process because material is already in WIP.""" + from erpnext.manufacturing.doctype.work_order.mapper import create_pick_list + from erpnext.stock.doctype.pick_list.mapper import create_stock_entry + + work_order = make_wo_order_test_record( + planned_start_date=now(), qty=2, source_warehouse="Stores - _TC" + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0 + ) + + pick_list = create_pick_list(work_order.name, for_qty=work_order.qty) + # pick only _Test Item; the other required item is left out of this pick list + pick_list.pick_manually = 1 + pick_list.locations = [loc for loc in pick_list.locations if loc.item_code == "_Test Item"] + pick_list.save() + pick_list.submit() + + stock_entry = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertEqual(stock_entry.fg_completed_qty, 0.0) + stock_entry.submit() + + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 0.0) + self.assertEqual(work_order.status, "In Process") + def test_backflushed_batch_raw_materials_based_on_transferred(self): frappe.db.set_single_value( "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index a7af812fd34..22da1a1d989 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1003,6 +1003,9 @@ class WorkOrder(Document): def update_transferred_qty_for_required_items(self): return RequiredItemsService(self).update_transferred_qty_for_required_items() + def refresh_material_transferred_for_manufacturing(self): + return RequiredItemsService(self).refresh_material_transferred_for_manufacturing() + def update_returned_qty(self): return RequiredItemsService(self).update_returned_qty() diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 680cb83b312..1f82ec847b3 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -2,6 +2,8 @@ # For license information, please see license.txt +from collections import defaultdict + import frappe from frappe import _ @@ -14,29 +16,47 @@ def execute(filters=None): def get_data(filters, data): - get_exploded_items(filters.bom, data) + children_map = fetch_exploded_bom_items(filters.bom) + build_exploded_rows(filters.bom, children_map, data) -def get_exploded_items(bom, data, indent=0, qty=1): - exploded_items = frappe.get_all( - "BOM Item", - filters={"parent": bom}, - fields=[ - "qty", - "bom_no", - "qty", - "item_code", - "item_name", - "description", - "uom", - "idx", - "is_phantom_item", - ], - order_by="idx ASC", +def fetch_exploded_bom_items(root_bom): + """Every BOM Item in the exploded tree of `root_bom`, grouped by its parent BOM, in one + recursive CTE -- replaces a query-per-node walk with a single query. UNION keeps it cycle-safe + and fetches each sub-BOM's items only once even when it is reused across the tree.""" + bom_item = frappe.qb.DocType("BOM Item") + tree = frappe.qb.Table("exploded_bom") + fields = [ + bom_item.parent, + bom_item.qty, + bom_item.bom_no, + bom_item.item_code, + bom_item.item_name, + bom_item.description, + bom_item.uom, + bom_item.idx, + bom_item.is_phantom_item, + ] + seed = frappe.qb.from_(bom_item).select(*fields).where(bom_item.parent == root_bom) + recursion = ( + frappe.qb.from_(bom_item) + .join(tree) + .on(bom_item.parent == tree.bom_no) + .select(*fields) + .where(tree.bom_no != "") ) + rows = ( + frappe.qb.with_(seed + recursion, "exploded_bom", recursive=True).from_(tree).select(tree.star) + ).run(as_dict=True) - for item in exploded_items: - item["indent"] = indent + children_map = defaultdict(list) + for row in rows: + children_map[row.parent].append(row) + return children_map + + +def build_exploded_rows(bom, children_map, data, indent=0, qty=1): + for item in sorted(children_map.get(bom, []), key=lambda row: row.idx): data.append( { "item_code": item.item_code, @@ -51,7 +71,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): } ) if item.bom_no: - get_exploded_items(item.bom_no, data, indent=indent + 1, qty=item.qty) + build_exploded_rows(item.bom_no, children_map, data, indent + 1, item.qty) def get_columns(): diff --git a/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py new file mode 100644 index 00000000000..54bfae2d6c2 --- /dev/null +++ b/erpnext/manufacturing/report/bom_explorer/test_bom_explorer.py @@ -0,0 +1,80 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom +from erpnext.manufacturing.report.bom_explorer.bom_explorer import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBOMExplorer(ERPNextTestSuite): + def setUp(self): + # the tests look up `_Test FG Item`'s BOM, which comes from the BOM fixtures; + # load them so the file also passes when run in isolation + self.load_test_records("BOM") + + def run_report(self, bom): + filters = frappe._dict({"bom": bom}) + return execute(filters)[1] + + def top_level_rows_by_item(self, data): + # key only the direct (indent 0) components, so an item that also appears in a + # deeper sub-assembly can't overwrite the top-level row we assert against + return {row["item_code"]: row for row in data if row["indent"] == 0} + + def test_default_bom_lists_components_at_top_level(self): + bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1}) + self.assertIsNotNone(bom, "Default active BOM for _Test FG Item not found") + + data = self.run_report(bom) + rows_by_item = self.top_level_rows_by_item(data) + + self.assertIn("_Test Item", rows_by_item) + self.assertIn("_Test Item Home Desktop 100", rows_by_item) + + for item_code in ("_Test Item", "_Test Item Home Desktop 100"): + row = rows_by_item[item_code] + self.assertEqual(row["indent"], 0) + self.assertEqual(row["bom_level"], 0) + + def test_qty_matches_bom_item_qty(self): + bom = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_active": 1, "is_default": 1}) + data = self.run_report(bom) + rows_by_item = self.top_level_rows_by_item(data) + + for bom_item in frappe.get_all( + "BOM Item", filters={"parent": bom}, fields=["item_code", "qty", "uom"] + ): + row = rows_by_item[bom_item.item_code] + self.assertEqual(row["qty"], bom_item.qty) + self.assertEqual(row["uom"], bom_item.uom) + + def test_nested_bom_shows_deeper_level(self): + # Sub-assembly: "sub" is itself a BOM containing "leaf". + parent_bom = create_nested_bom( + {"parent": {"sub": {"leaf": {}}, "flat": {}}}, + prefix="_Test explorer ", + ) + + data = self.run_report(parent_bom.name) + rows_by_item = {row["item_code"]: row for row in data} + + sub_item = "_Test explorer sub" + leaf_item = "_Test explorer leaf" + flat_item = "_Test explorer flat" + + self.assertIn(sub_item, rows_by_item) + self.assertIn(flat_item, rows_by_item) + self.assertIn(leaf_item, rows_by_item) + + # Direct components of the parent sit at level 0. + self.assertEqual(rows_by_item[flat_item]["indent"], 0) + self.assertEqual(rows_by_item[sub_item]["indent"], 0) + + # The sub-assembly row carries its own BOM reference. + self.assertTrue(rows_by_item[sub_item]["bom"]) + + # The leaf belongs to the sub-assembly, so it is exploded one level deeper. + self.assertEqual(rows_by_item[leaf_item]["indent"], 1) + self.assertEqual(rows_by_item[leaf_item]["bom_level"], 1) diff --git a/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py new file mode 100644 index 00000000000..10a55e4829d --- /dev/null +++ b/erpnext/manufacturing/report/bom_operations_time/test_bom_operations_time.py @@ -0,0 +1,117 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +import frappe + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_operations_time.bom_operations_time import execute +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + +OPERATION = "_Test BOM Ops Time Operation" +WORKSTATION = "_Test BOM Ops Time Workstation" +OTHER_OPERATION = "_Test BOM Ops Time Operation 2" +OTHER_WORKSTATION = "_Test BOM Ops Time Workstation 2" +TIME_IN_MINS = 45 + + +class TestBOMOperationsTime(ERPNextTestSuite): + def setUp(self): + ensure_workstation_and_operation(WORKSTATION, OPERATION) + self.rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name + self.fg_item = make_item(properties={"is_stock_item": 1}).name + self.bom = build_bom_with_operation(self.fg_item, self.rm_item, OPERATION, WORKSTATION) + + def run_report(self, **filters): + return execute(frappe._dict(filters))[1] + + def bom_names(self, rows): + return {row.name for row in rows} + + def build_other_bom(self): + """A submitted BOM for a different item, built on a different workstation.""" + ensure_workstation_and_operation(OTHER_WORKSTATION, OTHER_OPERATION) + other_fg = make_item(properties={"is_stock_item": 1}).name + return build_bom_with_operation(other_fg, self.rm_item, OTHER_OPERATION, OTHER_WORKSTATION) + + def test_operation_row_appears_with_expected_values(self): + rows = self.run_report(bom_id=[self.bom.name]) + + self.assertEqual(len(rows), 1) + row = rows[0] + self.assertEqual(row.name, self.bom.name) + self.assertEqual(row.item, self.fg_item) + self.assertEqual(row.operation, OPERATION) + self.assertEqual(row.workstation, WORKSTATION) + self.assertEqual(row.time_in_mins, TIME_IN_MINS) + + def test_item_code_filter_includes_matching_and_excludes_other(self): + other_bom = self.build_other_bom() + + # no bom_id here, so the item_code filter alone must scope the result + names = self.bom_names(self.run_report(item_code=self.fg_item)) + self.assertIn(self.bom.name, names) + self.assertNotIn(other_bom.name, names) + + # reverse direction: filtering the other item drops our BOM + other_names = self.bom_names(self.run_report(item_code=other_bom.item)) + self.assertIn(other_bom.name, other_names) + self.assertNotIn(self.bom.name, other_names) + + def test_workstation_filter_includes_matching_and_excludes_other(self): + other_bom = self.build_other_bom() + + # no bom_id here, so the workstation filter alone must scope the result + names = self.bom_names(self.run_report(workstation=WORKSTATION)) + self.assertIn(self.bom.name, names) + self.assertNotIn(other_bom.name, names) + + # reverse direction: filtering the other workstation drops our BOM + other_names = self.bom_names(self.run_report(workstation=OTHER_WORKSTATION)) + self.assertIn(other_bom.name, other_names) + self.assertNotIn(self.bom.name, other_names) + + def test_draft_bom_excluded(self): + draft_bom = build_bom_with_operation( + make_item(properties={"is_stock_item": 1}).name, + self.rm_item, + OPERATION, + WORKSTATION, + do_not_submit=True, + ) + + rows = self.run_report(bom_id=[draft_bom.name]) + self.assertEqual(rows, []) + + +def ensure_workstation_and_operation(workstation, operation): + if not frappe.db.exists("Workstation", workstation): + frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation}).insert( + ignore_permissions=True + ) + + if not frappe.db.exists("Operation", operation): + frappe.get_doc({"doctype": "Operation", "name": operation, "workstation": workstation}).insert( + ignore_permissions=True + ) + + +def build_bom_with_operation(fg_item, rm_item, operation, workstation, do_not_submit=False): + bom = make_bom( + item=fg_item, + raw_materials=[rm_item], + with_operations=1, + do_not_save=True, + ) + bom.append( + "operations", + { + "operation": operation, + "workstation": workstation, + "time_in_mins": TIME_IN_MINS, + "hour_rate": 100, + }, + ) + bom.insert(ignore_permissions=True) + if not do_not_submit: + bom.submit() + return bom diff --git a/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py new file mode 100644 index 00000000000..15e5e72d590 --- /dev/null +++ b/erpnext/manufacturing/report/bom_variance_report/test_bom_variance_report.py @@ -0,0 +1,110 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.bom_variance_report.bom_variance_report import execute +from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBOMVarianceReport(ERPNextTestSuite): + def setUp(self): + self.production_item = "_Test FG Item" + self.warehouse = "_Test Warehouse - _TC" + self.bom_no = frappe.db.get_value( + "BOM", {"item": self.production_item, "is_active": 1, "is_default": 1} + ) + self.raw_materials = self.get_bom_raw_materials() + + # allow over-production so a Work Order can produce more than planned; ERPNextTestSuite + # rolls this back at tearDown, so no manual restore is needed + frappe.db.set_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order", 100) + + def get_bom_raw_materials(self): + return { + row.item_code: row.qty + for row in frappe.get_all( + "BOM Item", filters={"parent": self.bom_no}, fields=["item_code", "qty"] + ) + } + + def create_over_produced_work_order(self, ordered_qty=2, produced_qty=3): + work_order = make_wo_order_test_record( + item=self.production_item, + qty=ordered_qty, + source_warehouse=self.warehouse, + skip_transfer=1, + ) + + for item_code in self.raw_materials: + test_stock_entry.make_stock_entry( + item_code=item_code, target=self.warehouse, qty=100, basic_rate=100 + ) + + stock_entry = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", produced_qty)) + stock_entry.submit() + + work_order.reload() + self.assertEqual(work_order.produced_qty, produced_qty) + return work_order + + def run_report(self, **extra): + filters = frappe._dict({"bom_no": self.bom_no, **extra}) + return execute(filters)[1] + + def test_over_produced_work_order_appears_with_planned_and_actual(self): + work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=3) + + data = self.run_report(work_order=work_order.name) + + summary_rows = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(len(summary_rows), 1) + + summary = summary_rows[0] + self.assertEqual(summary.get("production_item"), self.production_item) + self.assertEqual(summary.get("bom_no"), self.bom_no) + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 3) + + raw_material_rows = { + row.get("raw_material_code"): row for row in data if row.get("raw_material_code") + } + for item_code, per_unit_qty in self.raw_materials.items(): + self.assertIn(item_code, raw_material_rows) + # planned/required qty scales with the ordered qty on the work order + self.assertEqual(raw_material_rows[item_code].get("required_qty"), per_unit_qty * 2) + + def test_bom_no_filter_returns_over_produced_orders(self): + work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=3) + + data = self.run_report() + + matched = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(len(matched), 1) + self.assertEqual(matched[0].get("bom_no"), self.bom_no) + + def test_unstarted_work_order_is_excluded(self): + work_order = make_wo_order_test_record( + item=self.production_item, + qty=2, + source_warehouse=self.warehouse, + skip_transfer=1, + ) + + data = self.run_report(work_order=work_order.name) + + matched = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(matched, []) + + def test_work_order_produced_exactly_on_plan_is_excluded(self): + # the canonical no-variance case: produced qty equals the planned qty, so the + # report (which lists only over-produced orders) must not include it + work_order = self.create_over_produced_work_order(ordered_qty=2, produced_qty=2) + + data = self.run_report(work_order=work_order.name) + + matched = [row for row in data if row.get("work_order") == work_order.name] + self.assertEqual(matched, []) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index a86df319441..f95e7bcab5e 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -120,6 +120,12 @@ def get_columns(filters): "options": "Workstation", "width": "100", }, + { + "label": _("Hour Rate"), + "fieldtype": "Currency", + "fieldname": "hour_rate", + "width": "120", + }, { "label": _("Operating Cost"), "fieldtype": "Currency", diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py new file mode 100644 index 00000000000..191a57b01ed --- /dev/null +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/test_cost_of_poor_quality_report.py @@ -0,0 +1,130 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils.data import add_to_date, now + +from erpnext.manufacturing.doctype.job_card.mapper import make_corrective_job_card +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.cost_of_poor_quality_report.cost_of_poor_quality_report import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCostOfPoorQualityReport(ERPNextTestSuite): + """A Job Card appears in this report only when it is submitted (docstatus == 1) and flagged + as a corrective job card (is_corrective_job_card == 1). Such a card is created against a + corrective Operation (is_corrective_operation == 1); without any corrective operation the + report returns no rows at all.""" + + def setUp(self): + self.load_test_records("BOM") + + def create_corrective_job_card(self, hour_rate=100): + """Produce a submitted corrective Job Card and return (corrective_jc, operation, workstation).""" + work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2) + + job_card = frappe.get_last_doc("Job Card", {"work_order": work_order.name}) + job_card.append( + "time_logs", + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + ) + job_card.submit() + + corrective_operation = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_operation.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = hour_rate + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=2), + "to_time": add_to_date(now(), hours=2, minutes=30), + "completed_qty": 2, + }, + ) + corrective_job_card.submit() + + return corrective_job_card, corrective_operation.name, corrective_job_card.workstation + + def run_report(self, **filters): + return execute(frappe._dict(filters))[1] + + def test_corrective_job_card_is_listed_with_expected_fields(self): + corrective_jc, operation, workstation = self.create_corrective_job_card(hour_rate=100) + + rows = self.run_report(company="_Test Company") + row = next((r for r in rows if r["name"] == corrective_jc.name), None) + + self.assertIsNotNone(row, "Submitted corrective job card must appear in the report") + self.assertEqual(row["work_order"], corrective_jc.work_order) + self.assertEqual(row["operation"], operation) + self.assertEqual(row["workstation"], workstation) + self.assertEqual(row["item_code"], corrective_jc.production_item) + self.assertEqual(row["hour_rate"], 100) + self.assertEqual(row["total_time_in_mins"], corrective_jc.total_time_in_mins) + # operating_cost = hour_rate * total_time_in_mins / 60 (SQL float -> compare approximately) + self.assertAlmostEqual(row["operating_cost"], 100 * corrective_jc.total_time_in_mins / 60.0, places=6) + + def test_non_corrective_job_card_is_excluded(self): + corrective_jc, _operation, _workstation = self.create_corrective_job_card() + + # The regular (non-corrective) job card the corrective one was raised against must not appear. + regular_jc = corrective_jc.for_job_card + rows = self.run_report(company="_Test Company") + self.assertNotIn(regular_jc, {r["name"] for r in rows}) + + def test_operation_filter_scopes_rows(self): + corrective_jc, operation, _workstation = self.create_corrective_job_card() + + matching = self.run_report(company="_Test Company", operation=operation) + self.assertIn(corrective_jc.name, {r["name"] for r in matching}) + + other_operation = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + filtered = self.run_report(company="_Test Company", operation=other_operation.name) + self.assertNotIn(corrective_jc.name, {r["name"] for r in filtered}) + + def test_workstation_filter_scopes_rows(self): + corrective_jc, _operation, workstation = self.create_corrective_job_card() + + matching = self.run_report(company="_Test Company", workstation=workstation) + self.assertIn(corrective_jc.name, {r["name"] for r in matching}) + + filtered = self.run_report(company="_Test Company", workstation="__non_existent_ws__") + self.assertNotIn(corrective_jc.name, {r["name"] for r in filtered}) + + def test_work_order_and_name_filters_scope_rows(self): + corrective_jc, _operation, _workstation = self.create_corrective_job_card() + + by_work_order = self.run_report(company="_Test Company", work_order=corrective_jc.work_order) + self.assertIn(corrective_jc.name, {r["name"] for r in by_work_order}) + + by_name = self.run_report(company="_Test Company", name=corrective_jc.name) + self.assertEqual({r["name"] for r in by_name}, {corrective_jc.name}) + + def test_date_filter_scopes_rows(self): + corrective_jc, _operation, _workstation = self.create_corrective_job_card() + + # Time logs sit ~2 hours from now; a window covering today includes the card. + within = self.run_report( + company="_Test Company", + work_order=corrective_jc.work_order, + from_date=add_to_date(now(), days=-1), + to_date=add_to_date(now(), days=1), + ) + self.assertIn(corrective_jc.name, {r["name"] for r in within}) + + # A future-only window excludes it, proving the Job Card Time Log join filters by time. + outside = self.run_report( + company="_Test Company", + work_order=corrective_jc.work_order, + from_date=add_to_date(now(), days=5), + to_date=add_to_date(now(), days=6), + ) + self.assertNotIn(corrective_jc.name, {r["name"] for r in outside}) diff --git a/erpnext/manufacturing/report/downtime_analysis/test_downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/test_downtime_analysis.py new file mode 100644 index 00000000000..8c1a8292eab --- /dev/null +++ b/erpnext/manufacturing/report/downtime_analysis/test_downtime_analysis.py @@ -0,0 +1,92 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + + +import frappe +from frappe.utils import add_days, get_datetime, today + +from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation +from erpnext.manufacturing.report.downtime_analysis.downtime_analysis import execute +from erpnext.setup.doctype.employee.test_employee import make_employee +from erpnext.tests.utils import ERPNextTestSuite + + +class TestDowntimeAnalysis(ERPNextTestSuite): + def setUp(self): + self.workstation = make_workstation(workstation="_Test Downtime Workstation").name + self.other_workstation = make_workstation(workstation="_Test Downtime Workstation 2").name + self.operator = make_employee("test_downtime_operator@example.com", company="_Test Company") + + # from_time / to_time are two hours apart -> downtime of 120 minutes (2 hours). + self.from_time = get_datetime(f"{today()} 09:00:00") + self.to_time = get_datetime(f"{today()} 11:00:00") + self.entry = self.make_downtime_entry(self.workstation) + + def make_downtime_entry(self, workstation, **extra): + values = { + "doctype": "Downtime Entry", + "workstation": workstation, + "operator": self.operator, + "from_time": self.from_time, + "to_time": self.to_time, + "stop_reason": "Machine malfunction", + } + values.update(extra) + return frappe.get_doc(values).insert() + + def run_report(self, **extra): + filters = frappe._dict( + { + "from_date": add_days(today(), -1), + "to_date": add_days(today(), 1), + } + ) + filters.update(extra) + return execute(filters)[1] + + def row_for_entry(self, rows, name): + return next((row for row in rows if row.get("name") == name), None) + + def test_downtime_is_computed_in_hours(self): + # validate() stores downtime in minutes; the report converts it to hours. + self.assertEqual(self.entry.downtime, 120) + + row = self.row_for_entry(self.run_report(), self.entry.name) + self.assertIsNotNone(row, "Downtime Entry not present in report output") + self.assertEqual(row.get("workstation"), self.workstation) + self.assertEqual(row.get("operator"), self.operator) + self.assertEqual(row.get("stop_reason"), "Machine malfunction") + self.assertEqual(row.get("downtime"), 2.0) + + def test_workstation_filter_scopes_rows(self): + other = self.make_downtime_entry(self.other_workstation) + + rows = self.run_report(workstation=self.workstation) + names = {row.get("name") for row in rows} + self.assertIn(self.entry.name, names) + self.assertNotIn(other.name, names) + self.assertTrue(all(row.get("workstation") == self.workstation for row in rows)) + + def test_date_range_excludes_out_of_window_entries(self): + # The report filters from_time >= from_date and to_time <= to_date; a window + # ending before the entry's from_time must exclude it. + rows = self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5)) + self.assertIsNone(self.row_for_entry(rows, self.entry.name)) + + def test_chart_aggregates_downtime_per_workstation(self): + self.make_downtime_entry(self.workstation) + + chart = execute( + frappe._dict( + { + "from_date": add_days(today(), -1), + "to_date": add_days(today(), 1), + "workstation": self.workstation, + } + ) + )[3] + + self.assertIn(self.workstation, chart["data"]["labels"]) + index = chart["data"]["labels"].index(self.workstation) + # Two entries of 2 hours each for this workstation -> 4 hours aggregated. + self.assertEqual(chart["data"]["datasets"][0]["values"][index], 4.0) diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py new file mode 100644 index 00000000000..37d6c7da7ab --- /dev/null +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/test_exponential_smoothing_forecasting.py @@ -0,0 +1,101 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.manufacturing.report.exponential_smoothing_forecasting.exponential_smoothing_forecasting import ( + execute, +) +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + +FROM_DATE = "2026-06-01" +TO_DATE = "2026-08-31" +SMOOTHING_CONSTANT = 0.5 + + +class TestExponentialSmoothingForecasting(ERPNextTestSuite): + """Drive real submitted Sales Orders and assert the report buckets the ordered + quantities into the correct historical periods and produces a forecast.""" + + def setUp(self): + # The forecast query has no lower date bound, so it would pick up any committed + # Sales Order for the item. A uniquely-named item keeps the buckets scoped to + # just this test's orders. + self.item = make_item(properties={"is_stock_item": 1}).name + + def test_monthly_qty_forecast_from_sales_orders(self): + # Historical demand: distinct calendar months strictly before FROM_DATE. + # Monthly period keys are derived from the period's last day (e.g. "mar_2026"). + history = {"mar_2026": 7, "apr_2026": 4, "may_2026": 9} + self.create_sales_orders( + { + "2026-03-15": history["mar_2026"], + "2026-04-15": history["apr_2026"], + "2026-05-15": history["may_2026"], + } + ) + + columns, row = self.run_report() + fields = {col["fieldname"] for col in columns} + + # For Monthly periodicity only future periods are exposed as columns, each as a + # forecast_ field. Historical demand lives in the row data (keyed by month) but is + # not surfaced as its own column. + self.assertIn("forecast_jun_2026", fields, "expected future forecast column") + self.assertNotIn("jun_2026", fields, "future period must not expose raw demand column") + self.assertNotIn("mar_2026", fields, "historical month is not a Monthly report column") + + # Historical buckets must exactly reflect the ordered quantities. + for key, qty in history.items(): + self.assertEqual(flt(row.get(key)), flt(qty), f"bucket {key} mismatch") + + # The forecast seeds at the average of the non-zero historical months and then + # smooths through them in order: F = F + a*(actual - F). Asserting the exact + # analytical value pins the smoothing formula (Jun 2026 works out to ~7.2083). + expected_avg = sum(history.values()) / len(history) + self.assertAlmostEqual(flt(row.get("avg")), expected_avg, places=6) + + forecast = expected_avg + for month in ("mar_2026", "apr_2026", "may_2026"): + forecast = forecast + SMOOTHING_CONSTANT * (history[month] - forecast) + self.assertAlmostEqual(flt(row.get("forecast_jun_2026")), forecast, places=6) + + def test_ignores_documents_outside_range_and_other_docstatus(self): + self.create_sales_orders({"2026-05-10": 6}) + # A draft SO and a future-dated SO must not contribute to historical demand. + make_sales_order(item_code=self.item, qty=100, transaction_date="2026-05-20", do_not_submit=True) + make_sales_order(item_code=self.item, qty=100, transaction_date=FROM_DATE) + + _columns, row = self.run_report() + self.assertEqual(flt(row.get("may_2026")), 6.0) + + def create_sales_orders(self, date_to_qty): + for transaction_date, qty in date_to_qty.items(): + make_sales_order(item_code=self.item, qty=qty, transaction_date=transaction_date) + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "based_on_document": "Sales Order", + "based_on_field": "Qty", + "no_of_years": 3, + "periodicity": "Monthly", + "from_date": FROM_DATE, + "to_date": TO_DATE, + "smoothing_constant": SMOOTHING_CONSTANT, + "item_code": self.item, + } + ) + filters.update(extra) + + columns, data = execute(filters)[:2] + item_row = next( + (r for r in data if r.get("item_code") == self.item), + None, + ) + self.assertIsNotNone(item_row, f"{self.item} row missing from report output") + return columns, item_row diff --git a/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py new file mode 100644 index 00000000000..6e59a0d559d --- /dev/null +++ b/erpnext/manufacturing/report/job_card_summary/test_job_card_summary.py @@ -0,0 +1,87 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + + +import frappe +from frappe.utils import add_days, today + +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.job_card_summary.job_card_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestJobCardSummary(ERPNextTestSuite): + def setUp(self): + # `_Test FG Item 2` has a default active BOM with operations, so submitting a + # Work Order for it auto-creates Job Cards (one per operation). + self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2) + self.job_cards = frappe.get_all( + "Job Card", + filters={"work_order": self.work_order.name}, + fields=["name", "operation", "workstation", "production_item", "status"], + ) + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": add_days(today(), -1), + "to_date": add_days(today(), 1), + } + ) + filters.update(extra) + return execute(filters)[1] + + def rows_for_work_order(self, rows): + return [row for row in rows if row.get("work_order") == self.work_order.name] + + def test_job_cards_are_listed(self): + self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards") + + rows = self.rows_for_work_order(self.run_report()) + self.assertEqual(len(rows), len(self.job_cards)) + + reported_names = {row.get("name") for row in rows} + self.assertEqual(reported_names, {jc.name for jc in self.job_cards}) + + # Fresh (unsubmitted) job cards are reported as Open, and each row carries the + # operation / workstation / production item pulled from the Job Card. + for jc in self.job_cards: + row = next(row for row in rows if row.get("name") == jc.name) + self.assertEqual(row.get("status"), "Open") + self.assertEqual(row.get("operation"), jc.operation) + self.assertEqual(row.get("workstation"), jc.workstation) + self.assertEqual(row.get("production_item"), jc.production_item) + + def test_operation_filter_scopes_rows(self): + self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards") + operation = self.job_cards[0].operation + matching = {jc.name for jc in self.job_cards if jc.operation == operation} + + rows = self.rows_for_work_order(self.run_report(operation=operation)) + self.assertEqual({row.get("name") for row in rows}, matching) + + def test_status_filter(self): + self.assertTrue(self.job_cards, "Work Order did not produce any Job Cards") + + # The status filter matches the Job Card's *stored* status, so derive the + # expected set from that rather than assuming fresh cards are literally "Open". + stored_status = self.job_cards[0].status + expected = {jc.name for jc in self.job_cards if jc.status == stored_status} + + rows = self.rows_for_work_order(self.run_report(status=stored_status)) + self.assertEqual({row.get("name") for row in rows}, expected) + # any non-completed card is displayed as "Open" regardless of its stored status + for row in rows: + self.assertEqual(row.get("status"), "Open") + + # None of the freshly created job cards are Completed yet. + completed_rows = self.rows_for_work_order(self.run_report(status="Completed")) + self.assertEqual(completed_rows, []) + + def test_date_filter_excludes_out_of_range(self): + # Job Card posting_date defaults to today; a past-only window should exclude them. + rows = self.rows_for_work_order( + self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5)) + ) + self.assertEqual(rows, []) diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py index 2ba9f4742fd..084bcbf6060 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py @@ -50,11 +50,11 @@ def get_data(filters: Filters) -> Data: .groupby(se.work_order) ) - if "item" in filters: - query.where(wo.production_item == filters.item) + if filters.get("item"): + query = query.where(wo.production_item == filters.item) - if "work_order" in filters: - query.where(wo.name == filters.work_order) + if filters.get("work_order"): + query = query.where(wo.name == filters.work_order) data = query.run(as_dict=True) diff --git a/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py new file mode 100644 index 00000000000..d7ac8dd67e3 --- /dev/null +++ b/erpnext/manufacturing/report/process_loss_report/test_process_loss_report.py @@ -0,0 +1,114 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import nowdate + +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.process_loss_report.process_loss_report import execute +from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProcessLossReport(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": nowdate(), + "to_date": nowdate(), + } + ) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, work_order): + for row in data: + if row.get("name") == work_order: + return row + return None + + def make_manufactured_work_order(self, planned_qty, produced_qty): + """Create a submitted WO and manufacture `produced_qty` of `planned_qty`. + + The difference is booked as process loss on the Manufacture stock entry, + which propagates to the work order's `process_loss_qty`. + """ + wo_order = make_wo_order_test_record(production_item="_Test FG Item", qty=planned_qty) + + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=100, basic_rate=100 + ) + + transfer = frappe.get_doc( + make_stock_entry(wo_order.name, "Material Transfer for Manufacture", planned_qty) + ) + for d in transfer.get("items"): + d.s_warehouse = "Stores - _TC" + transfer.insert() + transfer.submit() + + manufacture = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", planned_qty)) + # Reduce the finished good qty below fg_completed_qty so the difference is + # recorded as process loss. + process_loss_qty = planned_qty - produced_qty + if process_loss_qty: + for d in manufacture.get("items"): + if d.is_finished_item: + d.qty = produced_qty + d.transfer_qty = produced_qty * (d.conversion_factor or 1) + manufacture.insert() + manufacture.submit() + + wo_order.reload() + return wo_order + + def test_work_order_with_process_loss_is_listed(self): + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4) + + self.assertEqual(wo_order.process_loss_qty, 1) + self.assertEqual(wo_order.produced_qty, 4) + + data = self.run_report() + row = self.find_row(data, wo_order.name) + + self.assertIsNotNone(row, "Work order with process loss should appear in the report") + self.assertEqual(row.production_item, "_Test FG Item") + self.assertEqual(row.qty_to_manufacture, 5) + self.assertEqual(row.produced_qty, 4) + self.assertEqual(row.process_loss_qty, 1) + + # total_pl_value = process_loss_qty * (total_fg_value / qty_to_manufacture) + expected_pl_value = row.process_loss_qty * (row.total_fg_value / row.qty_to_manufacture) + self.assertAlmostEqual(row.total_pl_value, expected_pl_value) + self.assertGreater(row.total_pl_value, 0) + + def test_work_order_without_process_loss_is_not_listed(self): + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=5) + + self.assertEqual(wo_order.process_loss_qty, 0) + self.assertEqual(wo_order.produced_qty, 5) + + data = self.run_report() + self.assertIsNone( + self.find_row(data, wo_order.name), + "Work order that produced the full planned qty should not appear (no loss)", + ) + + def test_item_filter_scopes_rows(self): + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4) + + # a matching production item includes the row, a non-matching one excludes it + self.assertIsNotNone(self.find_row(self.run_report(item="_Test FG Item"), wo_order.name)) + self.assertIsNone(self.find_row(self.run_report(item="_Test FG Item 2"), wo_order.name)) + + def test_work_order_filter_scopes_rows(self): + wo_order = self.make_manufactured_work_order(planned_qty=5, produced_qty=4) + + # the matching work order is included, a different work order name is excluded + self.assertIsNotNone(self.find_row(self.run_report(work_order=wo_order.name), wo_order.name)) + self.assertIsNone(self.find_row(self.run_report(work_order=f"{wo_order.name}-XX"), wo_order.name)) diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 9da87022e46..2fbd42210af 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -4,7 +4,7 @@ import frappe from frappe import _, scrub -from frappe.utils import getdate, today +from frappe.utils import get_datetime, getdate, today from erpnext.stock.report.stock_analytics.stock_analytics import ( get_period, @@ -31,7 +31,9 @@ def get_columns(period_columns): def get_work_orders(filters): from_date = filters.get("from_date") - to_date = filters.get("to_date") + # `creation` and `actual_end_date` are datetime columns, so a bare date upper + # bound would coerce to midnight and drop records created later on the last day. + to_date = get_datetime(filters.get("to_date")).replace(hour=23, minute=59, second=59) WorkOrder = frappe.qb.DocType("Work Order") diff --git a/erpnext/manufacturing/report/production_analytics/test_production_analytics.py b/erpnext/manufacturing/report/production_analytics/test_production_analytics.py new file mode 100644 index 00000000000..c02e94f249f --- /dev/null +++ b/erpnext/manufacturing/report/production_analytics/test_production_analytics.py @@ -0,0 +1,66 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe import _ +from frappe.utils import get_first_day, get_last_day, today + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProductionAnalytics(ERPNextTestSuite): + def run_report(self, **extra): + from erpnext.manufacturing.report.production_analytics.production_analytics import execute + + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": get_first_day(today()), + "to_date": get_last_day(today()), + "range": "Monthly", + } + ) + filters.update(extra) + columns, data, _msg, _chart = execute(filters) + return columns, data + + def get_period_count(self, columns, data, status, period_label): + """Return the count for a status row under the period column resolved by label.""" + period_fieldname = next(col["fieldname"] for col in columns if col.get("label") == period_label) + # the report stores the translated status label, so translate before matching + row = next(row for row in data if row["status"] == _(status)) + return row[period_fieldname] + + def test_submitted_work_order_increments_status_count(self): + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + + # pin the reporting window once so both runs use the same period even if the + # test happens to straddle a month boundary + from_date, to_date = get_first_day(today()), get_last_day(today()) + + # The current month is the period a newly created Work Order falls into (bucketed by creation date). + cols_before, data_before = self.run_report(from_date=from_date, to_date=to_date) + period_label = cols_before[-1]["label"] + before = self.get_period_count(cols_before, data_before, "Not Started", period_label) + + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + self.assertEqual(wo.docstatus, 1) + # A freshly submitted Work Order with no material transfer has status "Not Started". + self.assertEqual(wo.status, "Not Started") + + cols_after, data_after = self.run_report(from_date=from_date, to_date=to_date) + after = self.get_period_count(cols_after, data_after, "Not Started", period_label) + + self.assertEqual(after, before + 1) + + def test_report_shape(self): + columns, data = self.run_report() + + # First column is the Status column, followed by one column per period. + self.assertEqual(columns[0]["fieldname"], "status") + self.assertGreaterEqual(len(columns), 2) + + # One row per known Work Order status. + statuses = {row["status"] for row in data} + for status in ("Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"): + self.assertIn(_(status), statuses) diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index d7920af8141..82e150f807a 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -42,7 +42,9 @@ def get_production_plan_item_details(filters, data, order_details): order_qty = row.planned_qty total_produced_qty = 0.0 - pending_qty = 0.0 + # default to the full planned qty so a plan without any work order still + # reports everything as pending rather than a misleading zero + pending_qty = flt(order_qty) for work_order in work_orders: produced_qty = flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) pending_qty = flt(order_qty) - produced_qty diff --git a/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py new file mode 100644 index 00000000000..674c653b12e --- /dev/null +++ b/erpnext/manufacturing/report/production_plan_summary/test_production_plan_summary.py @@ -0,0 +1,130 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import create_production_plan +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry as make_se_from_wo +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.production_plan_summary.production_plan_summary import execute +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProductionPlanSummary(ERPNextTestSuite): + def run_report(self, production_plan): + filters = frappe._dict({"production_plan": production_plan}) + return execute(filters)[1] + + def make_plan(self, planned_qty=2): + return create_production_plan( + item_code="_Test FG Item", + planned_qty=planned_qty, + skip_getting_mr_items=1, + ) + + def make_submitted_work_order(self, plan, qty): + wo = make_wo_order_test_record( + item_code="_Test FG Item", + qty=qty, + company=plan.company, + wip_warehouse="Work In Progress - _TC", + fg_warehouse="Finished Goods - _TC", + skip_transfer=1, + use_multi_level_bom=1, + do_not_submit=True, + ) + wo.production_plan = plan.name + wo.production_plan_item = plan.po_items[0].name + wo.submit() + return wo + + def stock_required_materials(self, wo): + # make sure every raw material is available in its source warehouse before manufacturing, + # otherwise a clean database raises NegativeStockError + for item in wo.required_items: + make_stock_entry( + item_code=item.item_code, + to_warehouse=item.source_warehouse or "_Test Warehouse - _TC", + qty=item.required_qty + 10, + rate=100, + ) + + def get_work_order_row(self, data, item_code): + for row in data: + if row.get("item_code") == item_code and row.get("document_type") == "Work Order": + return row + return None + + def get_summary_row(self, data, item_code): + for row in data: + if row.get("item_code") == item_code and not row.get("document_type"): + return row + return None + + def test_summary_without_work_order(self): + """A submitted plan with no work order still yields a summary row for the planned item.""" + plan = self.make_plan(planned_qty=2) + + data = self.run_report(plan.name) + summary = self.get_summary_row(data, "_Test FG Item") + + self.assertIsNotNone(summary) + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 0) + # nothing produced yet, so the whole planned qty is pending + self.assertEqual(summary.get("pending_qty"), 2) + self.assertIsNone(self.get_work_order_row(data, "_Test FG Item")) + + def test_summary_with_pending_work_order(self): + """An unproduced work order shows full planned qty as pending.""" + plan = self.make_plan(planned_qty=2) + wo = self.make_submitted_work_order(plan, qty=2) + + data = self.run_report(plan.name) + wo_row = self.get_work_order_row(data, "_Test FG Item") + + self.assertIsNotNone(wo_row) + self.assertEqual(wo_row.get("document_name"), wo.name) + self.assertEqual(wo_row.get("qty"), 2) + self.assertEqual(wo_row.get("produced_qty"), 0) + self.assertEqual(wo_row.get("pending_qty"), 2) + + summary = self.get_summary_row(data, "_Test FG Item") + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 0) + + def test_summary_reflects_produced_qty(self): + """Producing part of the work order updates produced and pending quantities.""" + plan = self.make_plan(planned_qty=2) + wo = self.make_submitted_work_order(plan, qty=2) + self.stock_required_materials(wo) + + se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 1)) + se.submit() + + data = self.run_report(plan.name) + wo_row = self.get_work_order_row(data, "_Test FG Item") + + self.assertEqual(wo_row.get("document_name"), wo.name) + self.assertEqual(wo_row.get("produced_qty"), 1) + self.assertEqual(wo_row.get("pending_qty"), 1) + + summary = self.get_summary_row(data, "_Test FG Item") + self.assertEqual(summary.get("qty"), 2) + self.assertEqual(summary.get("produced_qty"), 1) + self.assertEqual(summary.get("pending_qty"), 1) + + def test_summary_scoped_to_its_own_plan(self): + """Each plan's report only reports its own work order documents.""" + plan_a = self.make_plan(planned_qty=2) + wo_a = self.make_submitted_work_order(plan_a, qty=2) + + plan_b = self.make_plan(planned_qty=3) + wo_b = self.make_submitted_work_order(plan_b, qty=3) + + data_a = self.run_report(plan_a.name) + document_names = {row.get("document_name") for row in data_a if row.get("document_name")} + + self.assertIn(wo_a.name, document_names) + self.assertNotIn(wo_b.name, document_names) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py new file mode 100644 index 00000000000..08401329126 --- /dev/null +++ b/erpnext/manufacturing/report/quality_inspection_summary/test_quality_inspection_summary.py @@ -0,0 +1,81 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, nowdate + +from erpnext.manufacturing.report.quality_inspection_summary.quality_inspection_summary import execute +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.quality_inspection.test_quality_inspection import ( + create_quality_inspection, + make_minimal_job_card, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestQualityInspectionSummary(ERPNextTestSuite): + def setUp(self): + super().setUp() + create_item("_Test Item") + self.job_card = make_minimal_job_card(production_item="_Test Item") + self.qi = create_quality_inspection( + item_code="_Test Item", + reference_type="Job Card", + reference_name=self.job_card, + status="Accepted", + ) + + def run_report(self, **extra): + filters = frappe._dict(extra) + return execute(filters)[1] + + def _rows_for_qi(self, data): + return [row for row in data if row.get("name") == self.qi.name] + + def test_appears_in_date_range(self): + data = self.run_report(from_date=add_days(nowdate(), -1), to_date=add_days(nowdate(), 1)) + rows = self._rows_for_qi(data) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row["status"], "Accepted") + self.assertEqual(row["item_code"], "_Test Item") + self.assertEqual(row["reference_type"], "Job Card") + self.assertEqual(row["reference_name"], self.job_card) + + def test_excluded_outside_date_range(self): + data = self.run_report(from_date=add_days(nowdate(), -10), to_date=add_days(nowdate(), -5)) + self.assertEqual(self._rows_for_qi(data), []) + + def test_status_filter_includes_matching(self): + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + status=["Accepted"], + ) + self.assertEqual(len(self._rows_for_qi(data)), 1) + + def test_status_filter_excludes_non_matching(self): + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + status=["Rejected"], + ) + self.assertEqual(self._rows_for_qi(data), []) + + def test_item_code_filter_includes_matching(self): + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + item_code=["_Test Item"], + ) + self.assertEqual(len(self._rows_for_qi(data)), 1) + + def test_item_code_filter_excludes_other_item(self): + other_item = frappe.generate_hash(length=10) + data = self.run_report( + from_date=add_days(nowdate(), -1), + to_date=add_days(nowdate(), 1), + item_code=[other_item], + ) + self.assertEqual(self._rows_for_qi(data), []) diff --git a/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py b/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py new file mode 100644 index 00000000000..825c80701fb --- /dev/null +++ b/erpnext/manufacturing/report/work_order_consumed_materials/test_work_order_consumed_materials.py @@ -0,0 +1,111 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, nowdate + +from erpnext.manufacturing.doctype.work_order.mapper import make_stock_entry +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.work_order_consumed_materials.work_order_consumed_materials import execute +from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWorkOrderConsumedMaterials(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": add_days(nowdate(), -1), + "to_date": add_days(nowdate(), 1), + } + ) + filters.update(extra) + return execute(filters)[1] + + def make_manufactured_work_order(self, qty=2): + """Create a submitted WO, stock its raw materials, transfer and fully manufacture it.""" + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=qty, company="_Test Company") + + for item in wo.required_items: + test_stock_entry.make_stock_entry( + item_code=item.item_code, + target=wo.wip_warehouse, + qty=item.required_qty, + basic_rate=100, + ) + + transfer = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", qty)) + transfer.insert() + transfer.submit() + + manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", qty)) + manufacture.insert() + manufacture.submit() + + wo.reload() + return wo + + def get_wo_rows(self, data, work_order): + """The report blanks parent fields after the first raw-material row, so match by raw + material's parent work order instead of the (blanked) `name` column.""" + return [row for row in data if row.get("parent") == work_order] + + def test_consumed_materials_reported_after_manufacture(self): + wo = self.make_manufactured_work_order(qty=2) + + # fully producing the WO consumes exactly the required quantity of each raw material + self.assertEqual(wo.produced_qty, 2) + + data = self.run_report() + rows = self.get_wo_rows(data, wo.name) + + self.assertEqual(len(rows), len(wo.required_items)) + + # pair rows to required items by sorting rather than a dict keyed on item code, so + # a BOM with two lines for the same component wouldn't silently collapse to one row + rows_sorted = sorted(rows, key=lambda r: (r["raw_material_item_code"], r["required_qty"])) + items_sorted = sorted(wo.required_items, key=lambda i: (i.item_code, i.required_qty)) + for row, item in zip(rows_sorted, items_sorted, strict=True): + self.assertEqual(row["raw_material_item_code"], item.item_code) + self.assertEqual(row["required_qty"], item.required_qty) + self.assertEqual(row["transferred_qty"], item.required_qty) + self.assertEqual(row["consumed_qty"], item.required_qty) + # no over-consumption in a clean full manufacture + self.assertEqual(row["extra_consumed_qty"], 0.0) + self.assertEqual(row["returned_qty"], 0.0) + + # parent columns are populated on the first row only + first = rows[0] + self.assertEqual(first["status"], wo.status) + self.assertEqual(first["production_item"], "_Test FG Item") + self.assertEqual(first["qty"], 2) + self.assertEqual(first["produced_qty"], 2) + + def test_work_order_filter_scopes_output(self): + wo = self.make_manufactured_work_order(qty=1) + + data = self.run_report(name=wo.name) + + parents = {row.get("parent") for row in data} + self.assertEqual(parents, {wo.name}) + self.assertTrue(data) + + def test_draft_work_order_is_excluded(self): + # report only lists WOs in status In Process / Completed / Stopped + draft = make_wo_order_test_record( + production_item="_Test FG Item", qty=1, company="_Test Company", do_not_submit=True + ) + + data = self.run_report() + self.assertNotIn(draft.name, {row.get("parent") for row in data}) + + def test_date_range_filter_excludes_work_order(self): + wo = self.make_manufactured_work_order(qty=1) + + # positive anchor: the WO shows up within the default (current) window + self.assertIn(wo.name, {row.get("parent") for row in self.run_report()}) + + # a window that ends before the WO was created must not include it + data = self.run_report(from_date=add_days(nowdate(), -10), to_date=add_days(nowdate(), -5)) + self.assertNotIn(wo.name, {row.get("parent") for row in data}) diff --git a/erpnext/manufacturing/report/work_order_summary/test_work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/test_work_order_summary.py new file mode 100644 index 00000000000..fa9271f5fd9 --- /dev/null +++ b/erpnext/manufacturing/report/work_order_summary/test_work_order_summary.py @@ -0,0 +1,60 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import add_days, today + +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.report.work_order_summary.work_order_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWorkOrderSummary(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": add_days(today(), -1), + "to_date": today(), + } + ) + filters.update(extra) + return execute(filters)[1] + + def test_work_order_appears_with_expected_fields(self): + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + + rows = {row["name"]: row for row in self.run_report()} + self.assertIn(wo.name, rows) + + row = rows[wo.name] + self.assertEqual(row["production_item"], "_Test FG Item") + self.assertEqual(row["qty"], 10) + self.assertEqual(row["produced_qty"], 0) + self.assertEqual(row["status"], "Not Started") + + def test_status_filter_excludes_other_statuses(self): + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + self.assertEqual(wo.status, "Not Started") + + # A "Completed" filter must not return a "Not Started" work order. + names = {row["name"] for row in self.run_report(status="Completed")} + self.assertNotIn(wo.name, names) + + # The matching status still returns it. + names = {row["name"] for row in self.run_report(status="Not Started")} + self.assertIn(wo.name, names) + + def test_date_range_excludes_work_order_outside_window(self): + wo = make_wo_order_test_record(production_item="_Test FG Item", qty=10, company="_Test Company") + + # A window entirely in the past cannot contain a WO created today. + names = { + row["name"] + for row in self.run_report(from_date=add_days(today(), -10), to_date=add_days(today(), -5)) + } + self.assertNotIn(wo.name, names) + + # A window that includes today does contain it. + names = {row["name"] for row in self.run_report()} + self.assertIn(wo.name, names) diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 35781672835..4586876bbc6 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -432,9 +432,10 @@ "type": "Link" } ], - "modified": "2026-05-05 11:00:26.131777", + "modified": "2026-06-14 13:44:07.420267", "modified_by": "Administrator", "module": "Manufacturing", + "module_onboarding": "Manufacturing Onboarding", "name": "Manufacturing", "number_cards": [ { @@ -458,6 +459,465 @@ "roles": [], "sequence_id": 8.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Manufacturing", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Manufacturing", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "list-tree", + "indent": 0, + "keep_closed": 0, + "label": "BOM", + "link_to": "BOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "factory", + "indent": 0, + "keep_closed": 0, + "label": "Work Order", + "link_to": "Work Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "person-standing", + "indent": 0, + "keep_closed": 0, + "label": "Job Card", + "link_to": "Job Card", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Stock Entry", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Material Planning", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item Lead Time", + "link_to": "Item Lead Time", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Plan", + "link_to": "Production Plan", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Forecasting", + "link_to": "Exponential Smoothing Forecasting", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Master Production Schedule", + "link_to": "Master Production Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Forecast", + "link_to": "Sales Forecast", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Planning Report", + "link_to": "Production Planning Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "tool", + "indent": 1, + "keep_closed": 1, + "label": "Tools", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Creator", + "link_to": "BOM Creator", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Update Tool", + "link_to": "BOM Update Tool", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Comparison Tool", + "link_to": "bom-comparison-tool", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Downtime Entry", + "link_to": "Downtime Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Planning Report", + "link_to": "Production Planning Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Work Order Summary", + "link_to": "Work Order Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection Summary", + "link_to": "Quality Inspection Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Downtime Analysis", + "link_to": "Downtime Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Job Card Summary", + "link_to": "Job Card Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Search", + "link_to": "BOM Search", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Production Analytics", + "link_to": "Production Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "BOM Operations Time", + "link_to": "BOM Operations Time", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Work Order Consumed Materials", + "link_to": "Work Order Consumed Materials", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Warehouse", + "link_to": "Warehouse", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Operation", + "link_to": "Operation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Workstation", + "link_to": "Workstation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Workstation Type", + "link_to": "Workstation Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Plant Floor", + "link_to": "Plant Floor", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Routing", + "link_to": "Routing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Manufacturing Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Manufacturing", "type": "Workspace" } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0d1c3f01025..9b13bbfefd1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -492,3 +492,4 @@ erpnext.patches.v16_0.rename_subscription_billing_period_fields erpnext.patches.v16_0.drop_redundant_serial_no_index_from_sabb erpnext.patches.v16_0.set_default_close_opportunity_after_days execute:frappe.db.set_single_value("Accounts Settings", "pcv_job_timeout", 3600) +erpnext.patches.v16_0.backfill_pick_list_transferred_qty diff --git a/erpnext/patches/v16_0/backfill_pick_list_transferred_qty.py b/erpnext/patches/v16_0/backfill_pick_list_transferred_qty.py new file mode 100644 index 00000000000..6d3155c4c1a --- /dev/null +++ b/erpnext/patches/v16_0/backfill_pick_list_transferred_qty.py @@ -0,0 +1,58 @@ +import frappe +from frappe.query_builder.functions import Sum +from frappe.utils import flt + + +def execute(): + StockEntry = frappe.qb.DocType("Stock Entry") + StockEntryDetail = frappe.qb.DocType("Stock Entry Detail") + + pick_lists = ( + frappe.qb.from_(StockEntry) + .select(StockEntry.pick_list) + .distinct() + .where((StockEntry.pick_list.isnotnull()) & (StockEntry.docstatus == 1)) + ).run(pluck=True) + + if not pick_lists: + return + + rows = ( + frappe.qb.from_(StockEntryDetail) + .join(StockEntry) + .on(StockEntryDetail.parent == StockEntry.name) + .select( + StockEntry.pick_list, + StockEntryDetail.item_code, + StockEntryDetail.s_warehouse, + Sum(StockEntryDetail.transfer_qty).as_("qty"), + ) + .where((StockEntry.pick_list.isin(pick_lists)) & (StockEntry.docstatus == 1)) + .groupby(StockEntry.pick_list, StockEntryDetail.item_code, StockEntryDetail.s_warehouse) + ).run(as_dict=True) + + transferred = {(r.pick_list, r.item_code, r.s_warehouse): flt(r.qty) for r in rows} + + items = frappe.get_all( + "Pick List Item", + filters={"parent": ("in", pick_lists), "picked_qty": (">", 0)}, + fields=["name", "parent", "item_code", "warehouse", "picked_qty"], + order_by="idx", + ) + + updates = {} + for row in items: + key = (row.parent, row.item_code, row.warehouse) + available = transferred.get(key, 0) + if available <= 0: + continue + qty = min(flt(row.picked_qty), available) + transferred[key] = available - qty + updates[row.name] = {"transferred_qty": qty} + + if not updates: + return + + frappe.db.auto_commit_on_many_writes = True + frappe.db.bulk_update("Pick List Item", updates) + frappe.db.auto_commit_on_many_writes = False diff --git a/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py b/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py index 7230334266e..3f6e30bcbc5 100644 --- a/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py +++ b/erpnext/patches/v16_0/complete_onboarding_steps_for_older_sites.py @@ -34,6 +34,7 @@ def complete_onboarding_steps_if_record_exists(steps): if ( step.action == "Create Entry" and step.reference_document + and frappe.db.exists("DocType", step.reference_document) and frappe.get_all(step.reference_document, limit=1) ): frappe.db.set_value("Onboarding Step", step.name, "is_complete", 1, update_modified=False) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 9eda760a4e7..c431af5cf11 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -9,7 +9,7 @@ from frappe import _, throw from frappe.desk.form.assign_to import clear, close_all_assignments from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Max, Min, Sum -from frappe.utils import add_days, add_to_date, cstr, date_diff, flt, get_link_to_form, getdate, today +from frappe.utils import add_days, add_to_date, date_diff, flt, get_link_to_form, getdate, today from frappe.utils.data import format_date from frappe.utils.nestedset import NestedSet @@ -247,25 +247,32 @@ class Task(NestedSet): def check_recursion(self): if self.flags.ignore_recursion_check: return - check_list = [["task", "parent"], ["parent", "task"]] - for d in check_list: - task_list, count = [self.name], 0 - while len(task_list) > count: - tasks = frappe.get_all( - "Task Depends On", - filters={d[1]: cstr(task_list[count])}, - fields=[d[0]], - as_list=True, - ) - count = count + 1 - for b in tasks: - if b[0] == self.name: - frappe.throw(_("Circular Reference Error"), CircularReferenceError) - if b[0]: - task_list.append(b[0]) + # "Task Depends On" is a directed edge (parent depends on `task`); a cycle exists if this + # task is reachable from itself along either direction. One recursive CTE per direction + # fetches the whole reachable set in a single query -- UNION makes it cycle-safe at any + # depth, so unlike the old per-node BFS it needs no arbitrary depth cap. + for select_field, filter_field in (("task", "parent"), ("parent", "task")): + if self._reaches_self(select_field, filter_field): + frappe.throw(_("Circular Reference Error"), CircularReferenceError) - if count == 15: - break + def _reaches_self(self, select_field: str, filter_field: str) -> bool: + depends_on = frappe.qb.DocType("Task Depends On") + tree = frappe.qb.Table("dependency_tree") + seed = ( + frappe.qb.from_(depends_on) + .select(depends_on[select_field].as_("node")) + .where(depends_on[filter_field] == self.name) + ) + recursion = ( + frappe.qb.from_(depends_on) + .join(tree) + .on(depends_on[filter_field] == tree.node) + .select(depends_on[select_field]) + ) + reachable = ( + frappe.qb.with_(seed + recursion, "dependency_tree", recursive=True).from_(tree).select(tree.node) + ).run(pluck=True) + return self.name in reachable def reschedule_dependent_tasks(self): end_date = self.exp_end_date or self.act_end_date diff --git a/erpnext/projects/report/project_summary/test_project_summary.py b/erpnext/projects/report/project_summary/test_project_summary.py new file mode 100644 index 00000000000..66dff87e1e6 --- /dev/null +++ b/erpnext/projects/report/project_summary/test_project_summary.py @@ -0,0 +1,65 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe import _ + +from erpnext.projects.report.project_summary.project_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestProjectSummary(ERPNextTestSuite): + """Lists projects with their total / completed / overdue task counts.""" + + def make_project(self): + return frappe.get_doc( + { + "doctype": "Project", + "project_name": f"_Test PS {frappe.generate_hash(length=6)}", + "company": "_Test Company", + } + ).insert() + + def make_task(self, project, status="Open"): + task = frappe.get_doc( + { + "doctype": "Task", + "subject": f"Task {frappe.generate_hash(length=6)}", + "project": project.name, + } + ).insert() + if status != "Open": + # set the status directly; the report counts tasks by their stored status + frappe.db.set_value("Task", task.name, "status", status) + return task + + def run_report(self, project): + return execute(frappe._dict({"name": project.name})) + + def project_row(self, project): + _columns, data, *_rest = self.run_report(project) + return next((r for r in data if r["name"] == project.name), None) + + def test_task_counts(self): + project = self.make_project() + self.make_task(project, "Completed") + self.make_task(project, "Completed") + self.make_task(project, "Open") + self.make_task(project, "Overdue") + + row = self.project_row(project) + self.assertIsNotNone(row, "Project missing from report") + self.assertEqual(row["total_tasks"], 4) + self.assertEqual(row["completed_tasks"], 2) + self.assertEqual(row["overdue_tasks"], 1) + + def test_report_summary_totals(self): + project = self.make_project() + self.make_task(project, "Completed") + self.make_task(project, "Open") + + _columns, _data, _message, _chart, report_summary = self.run_report(project) + summary = {s["label"]: s["value"] for s in report_summary} + self.assertEqual(summary[_("Total Tasks")], 2) + self.assertEqual(summary[_("Completed Tasks")], 1) + self.assertEqual(summary[_("Overdue Tasks")], 0) diff --git a/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py b/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py new file mode 100644 index 00000000000..c4ae6ecd9b3 --- /dev/null +++ b/erpnext/projects/report/timesheet_billing_summary/test_timesheet_billing_summary.py @@ -0,0 +1,64 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.projects.doctype.timesheet.test_timesheet import make_timesheet +from erpnext.projects.report.timesheet_billing_summary.timesheet_billing_summary import execute +from erpnext.setup.doctype.employee.test_employee import make_employee +from erpnext.tests.utils import ERPNextTestSuite + + +class TestTimesheetBillingSummary(ERPNextTestSuite): + """Lists submitted Timesheet Detail rows with working/billing hours and amount, + optionally grouped by date/project/employee.""" + + def setUp(self): + self.employee = make_employee("timesheet_billing@example.com", company="_Test Company") + self.project = frappe.get_doc( + { + "doctype": "Project", + "project_name": f"_Test TBS {frappe.generate_hash(length=6)}", + "company": "_Test Company", + } + ).insert() + + def make_ts(self, is_billable=1): + return make_timesheet( + self.employee, simulate=True, is_billable=is_billable, project=self.project.name + ) + + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company", "employee": self.employee}) + filters.update(extra) + return execute(filters)[1] + + def test_billable_timesheet_row(self): + ts = self.make_ts(is_billable=1) + detail = ts.time_logs[0] + + rows = [r for r in self.run_report() if r.get("timesheet") == ts.name] + self.assertTrue(rows, "Timesheet missing from report") + row = rows[0] + self.assertEqual(row["hours"], 2) + self.assertEqual(row["billing_hours"], detail.billing_hours) + self.assertEqual(row["billing_amount"], detail.billing_amount) + self.assertEqual(row["project"], self.project.name) + + def test_group_by_project_sums_hours(self): + self.make_ts(is_billable=1) + + data = self.run_report(group_by="project") + group_rows = [r for r in data if r.get("is_group") and r.get("project") == self.project.name] + self.assertTrue(group_rows, "Grouped project row missing") + self.assertEqual(group_rows[0]["hours"], 2) + + def test_draft_excluded_unless_requested(self): + ts = make_timesheet( + self.employee, simulate=True, is_billable=1, project=self.project.name, do_not_submit=True + ) + + # submitted-only by default: the draft timesheet is absent + self.assertNotIn(ts.name, {r.get("timesheet") for r in self.run_report()}) + # ... but included when draft timesheets are requested + self.assertIn(ts.name, {r.get("timesheet") for r in self.run_report(include_draft_timesheets=1)}) diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index aa0f2cf2e71..55a296d4d0d 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -18,6 +18,14 @@ "is_hidden": 0, "label": "Projects", "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Projects", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "hidden": 0, "is_query_report": 0, @@ -37,6 +45,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Project", + "link_count": 0, + "link_to": "Project", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Task", + "link_count": 0, + "link_to": "Task", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -59,6 +89,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Project Template", + "link_count": 0, + "link_to": "Project Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -70,6 +111,28 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Project Type", + "link_count": 0, + "link_to": "Project Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Project", + "hidden": 0, + "is_query_report": 0, + "label": "Project Update", + "link_count": 0, + "link_to": "Project Update", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Project", "hidden": 0, @@ -89,6 +152,14 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Time Tracking", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -100,6 +171,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Timesheet", + "link_count": 0, + "link_to": "Timesheet", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Activity Type", + "link_count": 0, + "link_to": "Activity Type", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -122,6 +215,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Activity Type", + "hidden": 0, + "is_query_report": 0, + "label": "Activity Cost", + "link_count": 0, + "link_to": "Activity Cost", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -130,6 +234,25 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Reports", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Timesheet", + "hidden": 0, + "is_query_report": 1, + "label": "Daily Timesheet Summary", + "link_count": 0, + "link_to": "Daily Timesheet Summary", + "link_type": "Report", + "onboard": 1, + "type": "Link" + }, { "dependencies": "Timesheet", "hidden": 0, @@ -152,6 +275,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Project", + "hidden": 0, + "is_query_report": 1, + "label": "Project wise Stock Tracking", + "link_count": 0, + "link_to": "Project wise Stock Tracking", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Project", "hidden": 0, @@ -163,6 +297,28 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Project", + "hidden": 0, + "is_query_report": 1, + "label": "Timesheet Billing Summary", + "link_count": 0, + "link_to": "Timesheet Billing Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Task", + "hidden": 0, + "is_query_report": 1, + "label": "Delayed Tasks Summary", + "link_count": 0, + "link_to": "Delayed Tasks Summary", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Task", "hidden": 0, @@ -182,6 +338,24 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 1, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Projects Settings", + "link_count": 0, + "link_to": "Projects Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -193,9 +367,10 @@ "type": "Link" } ], - "modified": "2026-01-02 17:26:44.644507", + "modified": "2026-07-01 13:20:50.651608", "modified_by": "Administrator", "module": "Projects", + "module_onboarding": "Projects Onboarding", "name": "Projects", "number_cards": [ { @@ -219,6 +394,250 @@ "roles": [], "sequence_id": 11.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Projects", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Project", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "projects", + "indent": 0, + "keep_closed": 0, + "label": "Project", + "link_to": "Project", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "list-todo", + "indent": 0, + "keep_closed": 0, + "label": "Task", + "link_to": "Task", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "calendar-clock", + "indent": 0, + "keep_closed": 0, + "label": "Timesheet", + "link_to": "Timesheet", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Activity Type", + "link_to": "Activity Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Activity Cost", + "link_to": "Activity Cost", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Project Template", + "link_to": "Project Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Project Type", + "link_to": "Project Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Project Update", + "link_to": "Project Update", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Project Summary", + "link_to": "Project Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Daily Timesheet Summary", + "link_to": "Daily Timesheet Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Timesheet Billing Summary", + "link_to": "Timesheet Billing Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Project wise Stock Tracking", + "link_to": "Project wise Stock Tracking", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Delayed Tasks Summary", + "link_to": "Delayed Tasks Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Projects Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Projects", "type": "Workspace" } diff --git a/erpnext/quality_management/workspace/quality/quality.json b/erpnext/quality_management/workspace/quality/quality.json index 43a9ca18759..adde0e308dc 100644 --- a/erpnext/quality_management/workspace/quality/quality.json +++ b/erpnext/quality_management/workspace/quality/quality.json @@ -161,7 +161,7 @@ "type": "Link" } ], - "modified": "2026-01-02 17:32:47.522875", + "modified": "2026-06-14 13:44:07.920643", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality", @@ -174,6 +174,161 @@ "roles": [], "sequence_id": 9.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Quality", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "inspection-panel", + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection", + "link_to": "Quality Inspection", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "goal", + "indent": 0, + "keep_closed": 0, + "label": "Quality Goal", + "link_to": "Quality Goal", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "review", + "indent": 0, + "keep_closed": 0, + "label": "Quality Review", + "link_to": "Quality Review", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "square-activity", + "indent": 0, + "keep_closed": 0, + "label": "Quality Action", + "link_to": "Quality Action", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "grid-2x2-check", + "indent": 0, + "keep_closed": 0, + "label": "Non Conformance", + "link_to": "Non Conformance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "thumbs-up", + "indent": 0, + "keep_closed": 0, + "label": "Quality Feedback", + "link_to": "Quality Feedback", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "users", + "indent": 0, + "keep_closed": 0, + "label": "Quality Meeting", + "link_to": "Quality Meeting", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Procedure", + "link_to": "Quality Procedure", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Feedback Template", + "link_to": "Quality Feedback Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection Template", + "link_to": "Quality Inspection Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Quality", "type": "Workspace" } diff --git a/erpnext/selling/doctype/customer/mapper.py b/erpnext/selling/doctype/customer/mapper.py index be69e7e5d6c..4f230702b6b 100644 --- a/erpnext/selling/doctype/customer/mapper.py +++ b/erpnext/selling/doctype/customer/mapper.py @@ -21,9 +21,6 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None): ) target_doc.quotation_to = "Customer" - target_doc.run_method("set_missing_values") - target_doc.run_method("set_other_charges") - target_doc.run_method("calculate_taxes_and_totals") price_list, currency = frappe.db.get_value( "Customer", {"name": source_name}, ["default_price_list", "default_currency"] @@ -33,6 +30,10 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None): if currency: target_doc.currency = currency + target_doc.run_method("set_missing_values") + target_doc.run_method("set_other_charges") + target_doc.run_method("calculate_taxes_and_totals") + return target_doc diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index e4b2ccae4d6..a1b15a1e867 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -5,7 +5,7 @@ import json import frappe -from frappe.utils import flt +from frappe.utils import flt, nowdate from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen @@ -14,12 +14,53 @@ from erpnext.selling.doctype.customer.customer import ( get_customer_outstanding, ) from erpnext.selling.doctype.customer.mapper import ( + make_quotation, parse_full_name, ) from erpnext.tests.utils import ERPNextTestSuite class TestCustomer(ERPNextTestSuite): + def test_quotation_from_customer_uses_actual_exchange_rate(self): + company = "_Test Company" + company_currency = frappe.get_cached_value("Company", company, "default_currency") + foreign_currency = "USD" if company_currency != "USD" else "EUR" + + frappe.defaults.set_user_default("company", company) + self.addCleanup(frappe.defaults.clear_user_default, "company") + + # Seed a deterministic rate so the test does not depend on the live exchange-rate API. + rate = 83.0 + exchange = frappe.get_doc( + { + "doctype": "Currency Exchange", + "date": nowdate(), + "from_currency": foreign_currency, + "to_currency": company_currency, + "exchange_rate": rate, + "for_selling": 1, + "for_buying": 1, + } + ).insert(ignore_if_duplicate=True) + self.addCleanup(frappe.delete_doc, "Currency Exchange", exchange.name, force=1) + + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test Customer FX Quotation", + "customer_type": "Company", + "default_currency": foreign_currency, + } + ).insert() + self.addCleanup(frappe.delete_doc, "Customer", customer.name, force=1) + + quotation = make_quotation(customer.name) + + self.assertEqual(quotation.currency, foreign_currency) + self.assertNotEqual(flt(quotation.conversion_rate), 1.0) + self.assertNotEqual(flt(quotation.conversion_rate), 0.0) + self.assertEqual(flt(quotation.conversion_rate), rate) + def test_get_customer_name_dedupes_with_numeric_suffix(self): # When a customer name already exists, get_customer_name appends "- ". The # Postgres branch extracts the suffix with regexp_replace/NULLIF/CAST (pypika's Substring cannot diff --git a/erpnext/selling/doctype/quotation/mapper.py b/erpnext/selling/doctype/quotation/mapper.py index 2182c969d4e..8ebdaf125d1 100644 --- a/erpnext/selling/doctype/quotation/mapper.py +++ b/erpnext/selling/doctype/quotation/mapper.py @@ -228,7 +228,7 @@ def _make_customer(source_name, ignore_permissions=False): def create_customer_from_lead(lead_name, ignore_permissions=False): - from erpnext.crm.doctype.lead.lead import _make_customer + from erpnext.crm.doctype.lead.mapper import _make_customer customer = _make_customer(lead_name, ignore_permissions=ignore_permissions) customer.flags.ignore_permissions = ignore_permissions diff --git a/erpnext/selling/report/customer_wise_item_price/test_customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/test_customer_wise_item_price.py new file mode 100644 index 00000000000..70181b19e79 --- /dev/null +++ b/erpnext/selling/report/customer_wise_item_price/test_customer_wise_item_price.py @@ -0,0 +1,69 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.report.customer_wise_item_price.customer_wise_item_price import execute +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + +PRICE_LIST = "Standard Selling" + + +class TestCustomerWiseItemPrice(ERPNextTestSuite): + """The report lists sales items with the selling rate from the customer's price + list and the available stock (summed across warehouses).""" + + def setUp(self): + self.item = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name + self.customer = self.create_customer() + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": self.item, + "price_list": PRICE_LIST, + "selling": 1, + "price_list_rate": 250, + } + ).insert() + make_stock_entry(item_code=self.item, to_warehouse="Stores - _TC", qty=10, rate=100) + + def create_customer(self): + name = "_Test CWIP Customer" + if not frappe.db.exists("Customer", name): + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": name, + "customer_group": "_Test Customer Group", + "territory": "_Test Territory", + "default_price_list": PRICE_LIST, + } + ).insert() + return name + + def run_report(self, **extra): + filters = frappe._dict({"customer": self.customer}) + filters.update(extra) + return execute(filters)[1] + + def test_customer_filter_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({})) + + def test_selling_rate_and_available_stock_for_item(self): + rows = self.run_report(item=self.item) + + row = next((r for r in rows if r["item_code"] == self.item), None) + self.assertIsNotNone(row, "Sales item missing from report") + self.assertEqual(row["item_name"], frappe.db.get_value("Item", self.item, "item_name")) + self.assertEqual(row["selling_rate"], 250) # from the customer's price list + self.assertEqual(row["available_stock"], 10) # stocked into Stores - _TC + self.assertEqual(row["price_list"], PRICE_LIST) + + def test_item_filter_scopes_to_single_item(self): + other = make_item(properties={"is_stock_item": 1, "is_sales_item": 1}).name + + item_codes = {r["item_code"] for r in self.run_report(item=self.item)} + self.assertIn(self.item, item_codes) + self.assertNotIn(other, item_codes) diff --git a/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py new file mode 100644 index 00000000000..4d599832573 --- /dev/null +++ b/erpnext/selling/report/item_wise_sales_history/test_item_wise_sales_history.py @@ -0,0 +1,123 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.doctype.sales_order.mapper import make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import ( + create_dn_against_so, + make_sales_order, +) +from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestItemWiseSalesHistory(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + **extra, + } + ) + return execute(filters) + + def so_row(self, so_name, **extra): + data = self.run_report(**extra)[1] + return next(row for row in data if row["sales_order"] == so_name) + + def test_sales_order_line_shown_with_values(self): + so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01") + + row = self.so_row(so.name) + self.assertEqual(row["item_code"], "_Test Item") + self.assertEqual(row["quantity"], 10) + self.assertEqual(row["rate"], 100) + self.assertEqual(row["amount"], 1000) + self.assertEqual(row["customer"], "_Test Customer") + + def test_draft_sales_order_excluded(self): + so = make_sales_order(transaction_date="2026-06-01", do_not_submit=True) + + names = {row["sales_order"] for row in self.run_report()[1]} + self.assertNotIn(so.name, names) + + def test_date_range_filters_on_transaction_date(self): + so = make_sales_order(transaction_date="2026-06-01") + + in_range = { + row["sales_order"] for row in self.run_report(from_date="2026-05-01", to_date="2026-07-01")[1] + } + self.assertIn(so.name, in_range) + + out_of_range = { + row["sales_order"] for row in self.run_report(from_date="2026-01-01", to_date="2026-03-01")[1] + } + self.assertNotIn(so.name, out_of_range) + + def test_item_code_filter(self): + so = make_sales_order( + transaction_date="2026-06-01", + item_list=[ + {"item_code": "_Test Item", "qty": 5, "rate": 100, "warehouse": "_Test Warehouse - _TC"}, + {"item_code": "_Test Item 2", "qty": 3, "rate": 200, "warehouse": "_Test Warehouse - _TC"}, + ], + ) + + item_codes = {row["item_code"] for row in self.run_report(item_code="_Test Item 2")[1]} + self.assertEqual(item_codes, {"_Test Item 2"}) + # the filtered-out line of the same order must not leak in + self.assertTrue( + all(row["sales_order"] == so.name for row in self.run_report(item_code="_Test Item 2")[1]) + ) + + def test_customer_filter(self): + make_sales_order(customer="_Test Customer 1", transaction_date="2026-06-01") + make_sales_order(customer="_Test Customer 2", transaction_date="2026-06-01") + + customers = {row["customer"] for row in self.run_report(customer="_Test Customer 1")[1]} + self.assertEqual(customers, {"_Test Customer 1"}) + + def test_delivered_quantity_reflects_delivery(self): + so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01") + create_dn_against_so(so.name, delivered_qty=4) + + self.assertEqual(self.so_row(so.name)["delivered_quantity"], 4) + + def test_billed_amount_reflects_invoice(self): + so = make_sales_order(qty=10, rate=100, transaction_date="2026-06-01") + si = make_sales_invoice(so.name) + si.insert() + si.submit() + + self.assertEqual(self.so_row(so.name)["billed_amount"], 1000) + + def test_amounts_reported_in_company_currency(self): + # a USD order must report rate/amount converted to the company's currency (base_* fields) + so = make_sales_order( + do_not_save=True, + currency="USD", + qty=10, + rate=100, + transaction_date="2026-06-01", + ) + so.conversion_rate = 80 + so.insert() + so.submit() + + row = self.so_row(so.name) + self.assertEqual(row["rate"], 8000) # 100 USD * 80 + self.assertEqual(row["amount"], 80000) # 10 * 100 USD * 80 + + def test_chart_aggregates_amount_per_item(self): + make_sales_order(item_code="_Test Item", qty=2, rate=100, transaction_date="2026-06-01") + make_sales_order(item_code="_Test Item", qty=3, rate=100, transaction_date="2026-06-01") + + chart = self.run_report(item_code="_Test Item")[3] + labels = chart["data"]["labels"] + values = chart["data"]["datasets"][0]["values"] + self.assertIn("_Test Item", labels) + # 2*100 + 3*100 aggregated for the item + self.assertEqual(values[labels.index("_Test Item")], 500) diff --git a/erpnext/selling/report/quotation_trends/test_quotation_trends.py b/erpnext/selling/report/quotation_trends/test_quotation_trends.py new file mode 100644 index 00000000000..4ff03a5b53c --- /dev/null +++ b/erpnext/selling/report/quotation_trends/test_quotation_trends.py @@ -0,0 +1,88 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.doctype.quotation.test_quotation import make_quotation +from erpnext.selling.report.quotation_trends.quotation_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +FISCAL_YEAR = "_Test Fiscal Year 2026" +TXN_DATE = "2026-06-01" + + +class TestQuotationTrends(ERPNextTestSuite): + """The trends report buckets submitted Quotation quantities/amounts by period + (Yearly/Monthly) for the chosen `based_on` dimension (Item, Customer, ...).""" + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": FISCAL_YEAR, + "based_on": "Item", + "period": "Yearly", + } + ) + filters.update(extra) + result = execute(filters) + columns, data = result[0], result[1] + labels = [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + return labels, data + + def _cell(self, data, key_label, key_value, col_label, labels): + """Value at column `col_label` for the row whose `key_label` column equals + `key_value`, or 0 when that row doesn't exist yet.""" + key_idx = labels.index(key_label) + col_idx = labels.index(col_label) + for row in data: + if row[key_idx] == key_value: + return row[col_idx] or 0 + return 0 + + def test_yearly_item_amount_and_total(self): + # Yearly period => a single " (Qty)"/"(Amt)" bucket plus Total(Qty)/Total(Amt). + labels, before = self.run_report() + qty_col = f"{FISCAL_YEAR} (Qty)" + amt_col = f"{FISCAL_YEAR} (Amt)" + before_qty = self._cell(before, "Item", "_Test Item", qty_col, labels) + before_amt = self._cell(before, "Item", "_Test Item", amt_col, labels) + before_tot_qty = self._cell(before, "Item", "_Test Item", "Total(Qty)", labels) + before_tot_amt = self._cell(before, "Item", "_Test Item", "Total(Amt)", labels) + + make_quotation(item="_Test Item", qty=4, rate=200, transaction_date=TXN_DATE) + + labels, after = self.run_report() + self.assertEqual(self._cell(after, "Item", "_Test Item", qty_col, labels) - before_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", amt_col, labels) - before_amt, 800) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Qty)", labels) - before_tot_qty, 4) + self.assertEqual(self._cell(after, "Item", "_Test Item", "Total(Amt)", labels) - before_tot_amt, 800) + + def test_monthly_lands_in_june_bucket(self): + # Monthly period => one bucket per month; a 2026-06-01 quotation hits "Jun (Qty)"/"(Amt)". + labels, before = self.run_report(period="Monthly") + before_jun_qty = self._cell(before, "Item", "_Test Item", "Jun (Qty)", labels) + before_jun_amt = self._cell(before, "Item", "_Test Item", "Jun (Amt)", labels) + before_may_qty = self._cell(before, "Item", "_Test Item", "May (Qty)", labels) + + make_quotation(item="_Test Item", qty=3, rate=100, transaction_date=TXN_DATE) + + labels, after = self.run_report(period="Monthly") + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Qty)", labels) - before_jun_qty, 3) + # the amount path is a separate SUM(base_net_amount) case, so assert it too + self.assertEqual(self._cell(after, "Item", "_Test Item", "Jun (Amt)", labels) - before_jun_amt, 300) + # nothing was quoted in May, so that bucket is unchanged + self.assertEqual(self._cell(after, "Item", "_Test Item", "May (Qty)", labels) - before_may_qty, 0) + + def test_based_on_customer_groups_amount_by_party(self): + # based_on Customer keys rows on the "Party" column (the customer id) + labels, before = self.run_report(based_on="Customer") + amt_col = f"{FISCAL_YEAR} (Amt)" + before_amt = self._cell(before, "Party", "_Test Customer", amt_col, labels) + + make_quotation( + party_name="_Test Customer", item="_Test Item", qty=2, rate=150, transaction_date=TXN_DATE + ) + + labels, after = self.run_report(based_on="Customer") + self.assertEqual(self._cell(after, "Party", "_Test Customer", amt_col, labels) - before_amt, 300) diff --git a/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py new file mode 100644 index 00000000000..b1385ca4f09 --- /dev/null +++ b/erpnext/selling/report/sales_person_commission_summary/test_sales_person_commission_summary.py @@ -0,0 +1,85 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.report.sales_person_commission_summary.sales_person_commission_summary import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestSalesPersonCommissionSummary(ERPNextTestSuite): + """The report joins a sales document (Sales Invoice/Order/Delivery Note) with its + Sales Team rows, listing each sales person's contribution and commission.""" + + def setUp(self): + # reuse the bootstrap sales persons (under the "Sales Team" group) + self.sales_person = "_Test Sales Person" + + def make_invoice_with_commission(self, percentage=100, commission_rate=5, incentives=50): + si = create_sales_invoice(rate=1000, qty=1, do_not_save=True, posting_date="2026-06-01") + si.append( + "sales_team", + { + "sales_person": self.sales_person, + "allocated_percentage": percentage, + "commission_rate": commission_rate, + "incentives": incentives, + }, + ) + si.insert() + si.submit() + si.reload() # reflect any values recomputed on submit + return si + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "doc_type": "Sales Invoice", + "sales_person": self.sales_person, + # scope to this test's posting date so the query isn't unbounded over + # every invoice for the shared sales person + "from_date": "2026-06-01", + "to_date": "2026-06-01", + } + ) + filters.update(extra) + return execute(filters)[1] + + def test_doc_type_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({"company": "_Test Company"})) + + def test_commission_row_matches_sales_team_entry(self): + si = self.make_invoice_with_commission(percentage=100, commission_rate=5, incentives=50) + team = si.sales_team[0] + + rows = self.run_report() + row = next((r for r in rows if r[0] == si.name), None) + self.assertIsNotNone(row, "Invoice with commission missing from report") + + # row: name, customer, territory, posting_date, base_net_amount, sales_person, + # allocated_percentage, commission_rate, allocated_amount, incentives + self.assertEqual(row[1], si.customer) + self.assertEqual(row[4], si.base_net_total) + self.assertEqual(row[5], self.sales_person) + self.assertEqual(row[6], team.allocated_percentage) + self.assertEqual(row[7], team.commission_rate) + self.assertEqual(row[8], team.allocated_amount) + self.assertEqual(row[9], team.incentives) + + def test_appends_total_row(self): + self.make_invoice_with_commission() + rows = self.run_report() + # the report appends a blank total row after one or more real data rows + self.assertGreaterEqual(len(rows), 2) + self.assertTrue(any(r[0] for r in rows[:-1]), "expected real data rows before the total row") + self.assertEqual(rows[-1], [""] * len(rows[0])) + + def test_sales_person_filter_scopes_rows(self): + si = self.make_invoice_with_commission() + + filtered = self.run_report(sales_person="_Test Sales Person 1") + self.assertNotIn(si.name, {r[0] for r in filtered if r[0]}) diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index f834f27df50..180740dcb6e 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -183,8 +183,22 @@ def get_entries(filters): .as_("contribution_amt") ) + # Only pass valid document-field filters to get_query; report-specific keys such as + # doc_type / sales_person / item_group are handled separately below. + doc_filters = {"docstatus": 1} + for field in ["company", "customer", "territory"]: + if filters.get(field): + doc_filters[field] = filters.get(field) + + if filters.get("from_date") and filters.get("to_date"): + doc_filters[date_field] = ["between", [filters.get("from_date"), filters.get("to_date")]] + elif filters.get("from_date"): + doc_filters[date_field] = [">=", filters.get("from_date")] + elif filters.get("to_date"): + doc_filters[date_field] = ["<=", filters.get("to_date")] + query = ( - frappe.get_query(dt, filters=filters, ignore_permissions=False) + frappe.get_query(dt, filters=doc_filters, ignore_permissions=False) .join(dt_item) .on(dt.name == dt_item.parent) .join(st) @@ -203,48 +217,29 @@ def get_entries(filters): contribution_amt_case, ) .where(st.parenttype == doc_type) - .where(dt.docstatus == 1) ) + if filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) + sp = frappe.qb.DocType("Sales Person") + query = query.where( + st.sales_person.isin(frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt))) + ) + + # only resolve items when an item_group/brand filter is set; otherwise get_items + # would return every item in the system and add a huge IN() clause on each run + if filters.get("item_group") or filters.get("brand"): + items = get_items(filters) + if not items: + # the item_group/brand filter matched nothing -> no rows + return [] + query = query.where(dt_item.item_code.isin([d[0] for d in items])) + query = query.orderby(st.sales_person).orderby(dt.name, order=frappe.qb.desc) return query.run(as_dict=True) -def get_conditions(filters, date_field): - conditions = [""] - values = [] - - for field in ["company", "customer", "territory"]: - if filters.get(field): - conditions.append(f"dt.{field}=%s") - values.append(filters[field]) - - if filters.get("sales_person"): - lft, rgt = frappe.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) - conditions.append( - f"exists(select name from `tabSales Person` where lft >= {lft} and rgt <= {rgt} and name=st.sales_person)" - ) - - if filters.get("from_date"): - conditions.append(f"dt.{date_field}>=%s") - values.append(filters["from_date"]) - - if filters.get("to_date"): - conditions.append(f"dt.{date_field}<=%s") - values.append(filters["to_date"]) - - items = get_items(filters) - if items: - conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items))) - values += items - else: - # return empty result, if no items are fetched after filtering on 'item group' and 'brand' - conditions.append("dt_item.item_code = Null") - - return " and ".join(conditions), values - - def get_items(filters): item = qb.DocType("Item") diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/test_sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/test_sales_person_wise_transaction_summary.py new file mode 100644 index 00000000000..2dbe8fee822 --- /dev/null +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/test_sales_person_wise_transaction_summary.py @@ -0,0 +1,69 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.report.sales_person_wise_transaction_summary.sales_person_wise_transaction_summary import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestSalesPersonWiseTransactionSummary(ERPNextTestSuite): + """Item-level summary joining a sales document with its Sales Team rows, showing + each sales person's contributed qty and amount per item line.""" + + def setUp(self): + self.sales_person = "_Test Sales Person" + + def make_invoice_with_commission(self, qty=5, rate=200, percentage=100): + si = create_sales_invoice( + item="_Test Item", qty=qty, rate=rate, do_not_save=True, posting_date="2026-06-01" + ) + si.append("sales_team", {"sales_person": self.sales_person, "allocated_percentage": percentage}) + si.insert() + si.submit() + return si + + def run_report(self, **extra): + filters = frappe._dict( + {"company": "_Test Company", "doc_type": "Sales Invoice", "sales_person": self.sales_person} + ) + filters.update(extra) + return execute(filters)[1] + + def test_doc_type_is_mandatory(self): + self.assertRaises(frappe.ValidationError, execute, frappe._dict({"company": "_Test Company"})) + + def test_invalid_doc_type_throws(self): + self.assertRaises( + frappe.ValidationError, + execute, + frappe._dict({"company": "_Test Company", "doc_type": "Purchase Invoice"}), + ) + + def test_item_line_contribution(self): + si = self.make_invoice_with_commission(qty=5, rate=200, percentage=100) + item = si.items[0] + + rows = self.run_report() + row = next((r for r in rows if r[0] == si.name and r[5] == "_Test Item"), None) + self.assertIsNotNone(row, "Invoice item line missing from report") + + # row: name, customer, territory, warehouse, posting_date, item_code, item_group, + # brand, stock_qty, base_net_amount, sales_person, allocated_percentage, + # contributed_qty, contribution_amt, currency + self.assertEqual(row[1], si.customer) + self.assertEqual(row[8], item.stock_qty) + self.assertEqual(row[9], item.base_net_amount) + self.assertEqual(row[10], self.sales_person) + self.assertEqual(row[11], 100) + self.assertEqual(row[12], item.stock_qty * 100 / 100) # contributed qty + self.assertEqual(row[13], item.base_net_amount * 100 / 100) # contribution amount + + def test_appends_total_row(self): + self.make_invoice_with_commission() + rows = self.run_report() + self.assertTrue(rows) + self.assertEqual(rows[-1], [""] * len(rows[0])) diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py b/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py new file mode 100644 index 00000000000..8c98a98bd7c --- /dev/null +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/test_territory_target_variance_based_on_item_group.py @@ -0,0 +1,68 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt, nowdate + +from erpnext.accounts.utils import get_fiscal_year +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import ( + create_target_distribution, +) +from erpnext.selling.report.territory_target_variance_based_on_item_group.territory_target_variance_based_on_item_group import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestTerritoryTargetVarianceBasedOnItemGroup(ERPNextTestSuite): + def setUp(self): + self.fiscal_year = get_fiscal_year(nowdate())[0] + + def test_achieved_target_and_variance(self): + distribution = create_target_distribution(self.fiscal_year) + territory = create_territory_with_target( + "_Test Target Territory", self.fiscal_year, distribution.name, target_qty=50 + ) + + # a Sales Order in that territory contributes to the achieved quantity + so = make_sales_order(rate=1000, qty=20, do_not_submit=True) + so.territory = territory.name + so.submit() + + result = execute( + frappe._dict( + { + "fiscal_year": self.fiscal_year, + "doctype": "Sales Order", + "period": "Yearly", + "target_on": "Quantity", + } + ) + )[1] + + # no item_group is set on the target, so the report emits exactly one row per + # territory -- assert all three figures against that single row + rows = [frappe._dict(r) for r in result if r.get("territory") == territory.name] + self.assertEqual(len(rows), 1, "expected exactly one row for the target territory") + row = rows[0] + self.assertEqual(flt(row.total_target, 2), 50) + self.assertEqual(flt(row.total_achieved, 2), 20) + self.assertEqual(flt(row.total_variance, 2), -30) + + +def create_territory_with_target(name, fiscal_year, distribution_id, target_qty=50): + doc = frappe.new_doc("Territory") + doc.territory_name = name + doc.parent_territory = "All Territories" + doc.is_group = 0 + doc.append( + "targets", + { + "fiscal_year": fiscal_year, + "target_qty": target_qty, + "target_amount": 30000, + "distribution_id": distribution_id, + }, + ) + return doc.insert() diff --git a/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py new file mode 100644 index 00000000000..8a069810b8d --- /dev/null +++ b/erpnext/selling/report/territory_wise_sales/test_territory_wise_sales.py @@ -0,0 +1,62 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.selling.doctype.quotation.test_quotation import make_quotation +from erpnext.selling.report.territory_wise_sales.territory_wise_sales import execute +from erpnext.tests.utils import ERPNextTestSuite + +TERRITORY = "_Test Territory" + + +class TestTerritoryWiseSales(ERPNextTestSuite): + """The report walks the Opportunity -> Quotation -> Sales Order -> Sales Invoice + funnel and totals each stage's amount per territory. + + These tests cover the Opportunity and Quotation stages; the Sales Order and + Sales Invoice (order_amount / billing_amount) stages are not yet exercised.""" + + def make_opportunity(self, amount=5000): + return frappe.get_doc( + { + "doctype": "Opportunity", + "opportunity_from": "Customer", + "party_name": "_Test Customer", + "territory": TERRITORY, + "company": "_Test Company", + "currency": "INR", + "opportunity_amount": amount, + "transaction_date": "2026-06-01", + } + ).insert() + + def make_quotation_for(self, opportunity, qty, rate): + qo = make_quotation(item="_Test Item", qty=qty, rate=rate, do_not_save=True) + qo.opportunity = opportunity.name + qo.insert() + qo.submit() + return qo + + def amount_for(self, territory, field): + for row in execute(frappe._dict({"company": "_Test Company"}))[1]: + if row["territory"] == territory: + return row[field] + return 0 + + def test_opportunity_amount_grouped_by_territory(self): + before = self.amount_for(TERRITORY, "opportunity_amount") + opp = self.make_opportunity(5000) + self.assertEqual(opp.territory, TERRITORY) + + after = self.amount_for(TERRITORY, "opportunity_amount") + self.assertEqual(after - before, 5000) + + def test_quotation_amount_flows_from_opportunity(self): + before = self.amount_for(TERRITORY, "quotation_amount") + + opp = self.make_opportunity() + quotation = self.make_quotation_for(opp, qty=2, rate=500) + + after = self.amount_for(TERRITORY, "quotation_amount") + self.assertEqual(after - before, quotation.base_grand_total) diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json index 141b3634708..b2b81a6e07c 100644 --- a/erpnext/selling/workspace/selling/selling.json +++ b/erpnext/selling/workspace/selling/selling.json @@ -622,9 +622,10 @@ "type": "Link" } ], - "modified": "2026-02-19 13:01:26.893303", + "modified": "2026-06-14 13:44:07.820564", "modified_by": "Administrator", "module": "Selling", + "module_onboarding": "Selling Onboarding", "name": "Selling", "number_cards": [ { @@ -648,6 +649,762 @@ "roles": [], "sequence_id": 6.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Selling", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Selling", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Quotation", + "link_to": "Quotation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sell", + "indent": 0, + "keep_closed": 0, + "label": "Sales Order", + "link_to": "Sales Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "receipt", + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "computer", + "indent": 1, + "keep_closed": 1, + "label": "POS", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "POS", + "link_to": "point-of-sale", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Profile", + "link_to": "POS Profile", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Invoice", + "link_to": "POS Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Opening Entry", + "link_to": "POS Opening Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Closing Entry", + "link_to": "POS Closing Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Invoice Merge Log", + "link_to": "POS Invoice Merge Log", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "POS Settings", + "link_to": "POS Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Loyalty Program", + "link_to": "Loyalty Program", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Loyalty Point Entry", + "link_to": "Loyalty Point Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 1, + "keep_closed": 1, + "label": "Items & Pricing", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Group", + "link_to": "Item Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Price List", + "link_to": "Price List", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Price", + "link_to": "Item Price", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Pricing Rule", + "link_to": "Pricing Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Promotional Scheme", + "link_to": "Promotional Scheme", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Coupon Code", + "link_to": "Coupon Code", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Blanket Order", + "link_to": "Blanket Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Group", + "link_to": "Customer Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Address", + "link_to": "Address", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Contact", + "link_to": "Contact", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Territory", + "link_to": "Territory", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Campaign", + "link_to": "Campaign", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person", + "link_to": "Sales Person", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partner", + "link_to": "Sales Partner", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Monthly Distribution", + "link_to": "Monthly Distribution", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Terms Template", + "link_to": "Terms and Conditions", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Tax Template", + "link_to": "Sales Taxes and Charges Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Product Bundle", + "link_to": "Product Bundle", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "UTM Source", + "link_to": "UTM Source", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Shipping Rule", + "link_to": "Shipping Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Register", + "link_to": "Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Sales Register", + "link_to": "Item-wise Sales Register", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Analytics", + "link_to": "Sales Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Addresses And Contacts", + "link_to": "Address And Contacts", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Inactive Customers", + "link_to": "Inactive Customers", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice Trends", + "link_to": "Sales Invoice Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Credit Balance", + "link_to": "Customer Credit Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customers Without Any Sales Transactions", + "link_to": "Customers Without Any Sales Transactions", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partners Commission", + "link_to": "Sales Partners Commission", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Available Stock for Packing Items", + "link_to": "Available Stock for Packing Items", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Territory Target Variance Based On Item Group", + "link_to": "Territory Target Variance Based On Item Group", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person Target Variance Based On Item Group", + "link_to": "Sales Person Target Variance Based On Item Group", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Partner Target Variance Based On Item Group", + "link_to": "Sales Partner Target Variance based on Item Group", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Pending SO Items For Purchase Request", + "link_to": "Pending SO Items For Purchase Request", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Funnel", + "link_to": "sales-funnel", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Order Analysis", + "link_to": "Sales Order Analysis", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Customer Acquisition and Loyalty", + "link_to": "Customer Acquisition and Loyalty", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Quotation Trends", + "link_to": "Quotation Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Order Trends", + "link_to": "Sales Order Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item-wise Sales History", + "link_to": "Item-wise Sales History", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Sales Person-wise Transaction Summary", + "link_to": "Sales Person-wise Transaction Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Selling Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Selling", "type": "Workspace" } diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 2173804a86f..84b79c95074 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -339,7 +339,7 @@ erpnext.company.setup_queries = function (frm) { ], [ "stock_delivered_but_not_billed", - { root_type: "Liability", account_type: "Stock Delivered But Not Billed" }, + { root_type: "Asset", account_type: "Stock Delivered But Not Billed" }, ], [ "service_received_but_not_billed", diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 1bf29c6801b..0036ea249ba 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -132,6 +132,7 @@ "default_purchase_price_variance_account", "default_manufacturing_variance_account", "stock_received_but_not_billed", + "enable_stock_delivered_but_not_billed", "stock_delivered_but_not_billed", "disable_sdbnb_in_sr", "default_provisional_account", @@ -353,33 +354,48 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_cost_center", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off Cost Center", + "no_copy": 1, "options": "Cost Center" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "write_off_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Write Off Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "exchange_gain_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Exchange Gain / Loss Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "unrealized_exchange_gain_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Unrealized Exchange Gain/Loss Account", + "no_copy": 1, "options": "Account" }, { @@ -526,15 +542,19 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "accumulated_depreciation_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Accumulated Depreciation Account", "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "depreciation_expense_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Depreciation Expense Account", "no_copy": 1, "options": "Account" @@ -549,29 +569,39 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "disposal_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Gain/Loss Account on Asset Disposal", "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "depreciation_cost_center", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Asset Depreciation Cost Center", "no_copy": 1, "options": "Cost Center" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "capital_work_in_progress_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Capital Work In Progress Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "asset_received_but_not_billed", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Asset Received But Not Billed", + "no_copy": 1, "options": "Account" }, { @@ -703,15 +733,21 @@ "options": "Warehouse" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "unrealized_profit_loss_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Unrealized Profit / Loss Account", + "no_copy": 1, "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "default_discount_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Payment Discount Account", + "no_copy": 1, "options": "Account" }, { @@ -753,8 +789,10 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_received_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Advance Received Account", "mandatory_depends_on": "book_advance_payments_as_liability", + "no_copy": 1, "options": "Account" }, { @@ -763,8 +801,10 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", "fieldname": "default_advance_paid_account", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Default Advance Paid Account", "mandatory_depends_on": "book_advance_payments_as_liability", + "no_copy": 1, "options": "Account" }, { @@ -844,9 +884,12 @@ "options": "Account" }, { + "depends_on": "eval:!doc.__islocal", "fieldname": "round_off_for_opening", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Round Off for Opening", + "no_copy": 1, "options": "Account" }, { @@ -1006,18 +1049,28 @@ }, { "default": "0", + "depends_on": "enable_stock_delivered_but_not_billed", "fieldname": "disable_sdbnb_in_sr", "fieldtype": "Check", "label": "Disable Stock Delivered But Not Billed in Sales Return", "no_copy": 1 }, { + "depends_on": "enable_stock_delivered_but_not_billed", "fieldname": "stock_delivered_but_not_billed", "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Stock Delivered But Not Billed", + "mandatory_depends_on": "enable_stock_delivered_but_not_billed", "no_copy": 1, "options": "Account" + }, + { + "default": "0", + "description": "If enabled, the value of goods delivered before invoicing will be recorded in the Stock Delivered But Not Billed account.", + "fieldname": "enable_stock_delivered_but_not_billed", + "fieldtype": "Check", + "label": "Enable Stock Delivered But Not Billed" } ], "grid_page_length": 50, @@ -1026,7 +1079,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2026-07-01 11:48:07.853494", + "modified": "2026-07-02 07:21:21.794533", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 2670de73902..5774d2cf09a 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -84,6 +84,7 @@ class Company(NestedSet): default_operating_cost_account: DF.Link | None default_payable_account: DF.Link | None default_provisional_account: DF.Link | None + default_purchase_price_variance_account: DF.Link | None default_receivable_account: DF.Link | None default_sales_contact: DF.Link | None default_scrap_warehouse: DF.Link | None @@ -99,6 +100,7 @@ class Company(NestedSet): enable_item_wise_inventory_account: DF.Check enable_perpetual_inventory: DF.Check enable_provisional_accounting_for_non_stock_items: DF.Check + enable_stock_delivered_but_not_billed: DF.Check exception_budget_approver_role: DF.Link | None exchange_gain_loss_account: DF.Link | None existing_company: DF.Link | None @@ -185,6 +187,64 @@ class Company(NestedSet): self.validate_inventory_account_settings() self.cant_change_valuation_method() self.validate_pending_reposts(old_doc) + self.validate_sdbnb_configuration() + + def validate_outstanding_sdbnb_transactions(self, account): + GLEntry = frappe.qb.DocType("GL Entry") + DeliveryNote = frappe.qb.DocType("Delivery Note") + + delivery_notes = ( + frappe.qb.from_(GLEntry) + .join(DeliveryNote) + .on((GLEntry.voucher_type == "Delivery Note") & (GLEntry.voucher_no == DeliveryNote.name)) + .select(DeliveryNote.name) + .where( + (GLEntry.is_cancelled == 0) + & (GLEntry.company == self.name) + & (GLEntry.account == account) + & (DeliveryNote.per_billed < 100) + & (DeliveryNote.docstatus == 1) + & (DeliveryNote.status.isin(["To Bill", "Partially Billed"])) + ) + .distinct() + .run(pluck=True) + ) + + if delivery_notes: + dn_links = ", ".join(get_link_to_form("Delivery Note", dn) for dn in delivery_notes[:10]) + + frappe.throw( + _( + "Stock Delivered But Not Billed Account cannot be changed or disabled since account {0} contains outstanding Delivery Notes: {1}" + ).format( + bold(account), + dn_links, + ) + ) + + def validate_sdbnb_configuration(self): + if self.get("__islocal"): + return + + if self.enable_stock_delivered_but_not_billed and not self.stock_delivered_but_not_billed: + frappe.throw(_("Please select Stock Delivered But Not Billed Account")) + + doc_before_save = self.get_doc_before_save() + + if not (doc_before_save and doc_before_save.stock_delivered_but_not_billed): + return + + account_changed = ( + self.stock_delivered_but_not_billed != doc_before_save.stock_delivered_but_not_billed + ) + + feature_disabled = ( + doc_before_save.enable_stock_delivered_but_not_billed + and not self.enable_stock_delivered_but_not_billed + ) + + if account_changed or feature_disabled: + self.validate_outstanding_sdbnb_transactions(doc_before_save.stock_delivered_but_not_billed) def cant_change_valuation_method(self): doc_before_save = self.get_doc_before_save() diff --git a/erpnext/setup/doctype/company/company_list.js b/erpnext/setup/doctype/company/company_list.js index 558e7500e3a..e69de29bb2d 100644 --- a/erpnext/setup/doctype/company/company_list.js +++ b/erpnext/setup/doctype/company/company_list.js @@ -1,5 +0,0 @@ -frappe.listview_settings["Company"] = { - onload() { - frappe.breadcrumbs.add("Accounts"); - }, -}; diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index bdb87e4bfdc..64f4974ef1f 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -10,7 +10,11 @@ from frappe.utils import random_string from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( get_charts_for_country, ) +from erpnext.accounts.doctype.account.test_account import create_account from erpnext.setup.doctype.company.company import get_default_company_address +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.tests.utils import ERPNextTestSuite @@ -234,6 +238,44 @@ class TestCompany(ERPNextTestSuite): after = get_all_transactions_annual_history(company).get(key, 0) self.assertEqual(after - before, 2) + def test_sdbnb_validation_requires_account_when_enabled(self): + company = get_test_company() + + company.enable_stock_delivered_but_not_billed = 1 + company.stock_delivered_but_not_billed = None + + with self.assertRaises(frappe.ValidationError): + company.save() + + def test_disable_sdbnb_with_outstanding_delivery_note_fails(self): + company = get_test_company() + + item_code = create_stock_item_with_inventory() + create_outstanding_delivery_note(item_code) + + company.enable_stock_delivered_but_not_billed = 0 + + with self.assertRaises(frappe.ValidationError): + company.save() + + def test_cannot_change_sdbnb_account_with_outstanding_delivery_note(self): + company = get_test_company() + + item_code = create_stock_item_with_inventory() + create_outstanding_delivery_note(item_code) + + new_account = create_account( + account_name="Stock Delivered But Not Billed - New", + account_type="Stock Delivered But Not Billed", + parent_account="Stock Assets - _TSDBNB", + company=company.name, + ) + + company.stock_delivered_but_not_billed = new_account + + with self.assertRaises(frappe.ValidationError): + company.save() + def test_demo_data(self): from erpnext.setup.demo import clear_demo_data, setup_demo_data @@ -297,3 +339,49 @@ def create_test_lead_in_company(company): lead.company = company lead.save() return lead.name + + +def get_test_company(): + if frappe.db.exists("Company", "_Test SDBNB Company"): + return frappe.get_doc("Company", "_Test SDBNB Company") + + return frappe.get_doc( + { + "doctype": "Company", + "company_name": "_Test SDBNB Company", + "abbr": "_TSDBNB", + "country": "India", + "default_currency": "INR", + "enable_perpetual_inventory": 1, + "enable_stock_delivered_but_not_billed": 1, + } + ).insert() + + +def create_stock_item_with_inventory(): + item_code = make_item( + "SDBNB Test Item", + properties={"is_stock_item": 1}, + ).name + + make_stock_entry( + item_code=item_code, + target="Stores - _TSDBNB", + qty=10, + basic_rate=100, + company="_Test SDBNB Company", + ) + + return item_code + + +def create_outstanding_delivery_note(item_code): + return create_delivery_note( + item_code=item_code, + qty=5, + rate=150, + company="_Test SDBNB Company", + warehouse="Stores - _TSDBNB", + cost_center="Main - _TSDBNB", + expense_account="Stock Delivered But Not Billed - _TSDBNB", + ) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index d3faeec4672..794175c81ce 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -223,5 +223,17 @@ "doctype": "Company", "chart_of_accounts": "Standard", "create_chart_of_accounts_based_on": "Standard Template" + }, + { + "abbr": "_TSDBNB", + "company_name": "_Test SDBNB Company", + "country": "India", + "default_currency": "INR", + "doctype": "Company", + "domain": "Manufacturing", + "chart_of_accounts": "Standard", + "default_holiday_list": "_Test Holiday List", + "enable_perpetual_inventory": 1, + "enable_stock_delivered_but_not_billed": 1 } -] +] \ No newline at end of file diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json index 745d0ad4d24..7398a65b56e 100644 --- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json +++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json @@ -69,7 +69,7 @@ "type": "Link" } ], - "modified": "2026-01-09 13:05:08.007297", + "modified": "2026-06-14 13:43:50.429297", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", @@ -128,6 +128,236 @@ "type": "DocType" } ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "earth", + "indent": 0, + "keep_closed": 0, + "label": "Global Defaults", + "link_to": "Global Defaults", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "washing-machine", + "indent": 0, + "keep_closed": 0, + "label": "System Settings", + "link_to": "System Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "accounting", + "indent": 0, + "keep_closed": 0, + "label": "Accounts Settings", + "link_to": "Accounts Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "computer", + "indent": 0, + "keep_closed": 0, + "label": "POS Settings", + "link_to": "POS Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "sell", + "indent": 0, + "keep_closed": 0, + "label": "Selling Settings", + "link_to": "Selling Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "buying", + "indent": 0, + "keep_closed": 0, + "label": "Buying Settings", + "link_to": "Buying Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Stock Settings", + "link_to": "Stock Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "building-2", + "indent": 0, + "keep_closed": 0, + "label": "Manufacturing Settings", + "link_to": "Manufacturing Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "projects", + "indent": 0, + "keep_closed": 0, + "label": "Projects Settings", + "link_to": "Projects Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "crm", + "indent": 0, + "keep_closed": 0, + "label": "CRM Settings", + "link_to": "CRM Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "support", + "indent": 0, + "keep_closed": 0, + "label": "Support Settings", + "link_to": "Support Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "getting-started", + "indent": 1, + "keep_closed": 1, + "label": "Other Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Subscription Settings", + "link_to": "Subscription Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item Variant Settings", + "link_to": "Item Variant Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Settings", + "link_to": "Delivery Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Currency Exchange Settings", + "link_to": "Currency Exchange Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Appointment Booking Settings", + "link_to": "Appointment Booking Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Stock Reposting Settings", + "link_to": "Stock Reposting Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "ERPNext Settings", "type": "Workspace" } diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 8c011f32d29..9b1f186e934 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -1,7 +1,7 @@ { "app": "erpnext", "charts": [], - "content": "[{\"id\":\"aCk49ShVRs\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Home\",\"col\":12}},{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"WU4F-HUcIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", + "content": "[{\"id\":\"kb3XPLg8lb\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"nWd2KJPW8l\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"snrzfbFr5Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"SHJKakmLLf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":3}},{\"id\":\"CPxEyhaf3G\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"d_KVM1gsf9\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"JVu8-FJZCu\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"JiuSi0ubOg\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"id\":\"ji2Jlm3Q8i\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"id\":\"N61oiXpuwK\",\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"id\":\"6J0CVl1mPo\",\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]", "creation": "2020-01-23 13:46:38.833076", "custom_blocks": [], "docstatus": 0, @@ -13,6 +13,14 @@ "is_hidden": 0, "label": "Home", "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Accounting", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "hidden": 0, "is_query_report": 0, @@ -32,6 +40,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Chart of Accounts", + "link_count": 0, + "link_to": "Account", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Company", + "link_count": 0, + "link_to": "Company", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -54,6 +84,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customer", + "link_count": 0, + "link_to": "Customer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Supplier", + "link_count": 0, + "link_to": "Supplier", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -73,6 +125,14 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Stock", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -84,6 +144,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Item", + "link_count": 0, + "link_to": "Item", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Warehouse", + "link_count": 0, + "link_to": "Warehouse", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -106,6 +188,17 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Brand", + "link_count": 0, + "link_to": "Brand", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -117,6 +210,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Unit of Measure (UOM)", + "link_count": 0, + "link_to": "UOM", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Stock Reconciliation", + "link_count": 0, + "link_to": "Stock Reconciliation", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -136,6 +251,25 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "CRM", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Lead", + "link_count": 0, + "link_to": "Lead", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -158,6 +292,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Customer Group", + "link_count": 0, + "link_to": "Customer Group", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Territory", + "link_count": 0, + "link_to": "Territory", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -177,6 +333,14 @@ "onboard": 0, "type": "Card Break" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Data Import and Settings", + "link_count": 0, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -188,6 +352,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Import Data", + "link_count": 0, + "link_to": "Data Import", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Opening Invoice Creation Tool", + "link_count": 0, + "link_to": "Opening Invoice Creation Tool", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -210,6 +396,28 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Chart of Accounts Importer", + "link_count": 0, + "link_to": "Chart of Accounts Importer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Letter Head", + "link_count": 0, + "link_to": "Letter Head", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -221,6 +429,17 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Email Account", + "link_count": 0, + "link_to": "Email Account", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -233,7 +452,7 @@ "type": "Link" } ], - "modified": "2025-07-02 14:12:28.407612", + "modified": "2026-07-01 14:22:16.927245", "modified_by": "Administrator", "module": "Setup", "name": "Home", @@ -267,6 +486,74 @@ "type": "DocType" } ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Home", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Customer", + "link_to": "Customer", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Supplier", + "link_to": "Supplier", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Sales Invoice", + "link_to": "Sales Invoice", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Home", "type": "Workspace" } diff --git a/erpnext/setup/workspace/organization/organization.json b/erpnext/setup/workspace/organization/organization.json new file mode 100644 index 00000000000..50cab83acdb --- /dev/null +++ b/erpnext/setup/workspace/organization/organization.json @@ -0,0 +1,204 @@ +{ + "allowed_users": [ + { + "user": "Administrator" + }, + { + "user": "Guest" + }, + { + "user": "accounts@test.com" + }, + { + "user": "ankush@erpnext.com" + }, + { + "user": "faris@erpnext.com" + }, + { + "user": "mention_test_user@example.com" + }, + { + "user": "project@frappe.io" + }, + { + "user": "rushabh@erpnext.com" + }, + { + "user": "saqib@erpnext.com" + }, + { + "user": "soham@frappe.io" + }, + { + "user": "sohamengineer123@gmail.com" + }, + { + "user": "sohamkulkarns9@gmail.com" + }, + { + "user": "sydel@frappe.io" + }, + { + "user": "test'5@example.com" + }, + { + "user": "test1@example.com" + }, + { + "user": "test2@example.com" + }, + { + "user": "test3@example.com" + }, + { + "user": "test4@example.com" + }, + { + "user": "test@example.com" + }, + { + "user": "test@portal.com" + }, + { + "user": "testpassword@example.com" + }, + { + "user": "testperm@example.com" + }, + { + "user": "web@web.com" + } + ], + "app": "erpnext", + "charts": [], + "content": "[]", + "creation": "2026-06-11 11:51:21.789012", + "custom_blocks": [], + "docstatus": 0, + "doctype": "Workspace", + "for_user": "", + "hide_custom": 0, + "icon": "organization", + "idx": 0, + "indicator_color": "green", + "is_hidden": 0, + "label": "Organization", + "link_type": "DocType", + "links": [], + "modified": "2026-06-16 00:45:57.595188", + "modified_by": "Administrator", + "module": "Setup", + "module_onboarding": "Organization Onboarding", + "name": "Organization", + "number_cards": [], + "owner": "Administrator", + "public": 1, + "quick_lists": [], + "roles": [], + "sequence_id": 46.0, + "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 1, + "icon": "organization", + "indent": 0, + "keep_closed": 0, + "label": "Company", + "link_to": "Company", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "book-text", + "indent": 0, + "keep_closed": 0, + "label": "Letter Head", + "link_to": "Letter Head", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "file-user", + "indent": 0, + "keep_closed": 0, + "label": "Department", + "link_to": "Department", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "book-user", + "indent": 0, + "keep_closed": 0, + "label": "Branch", + "link_to": "Branch", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "users", + "indent": 0, + "keep_closed": 0, + "label": "User", + "link_to": "User", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "user-round-check", + "indent": 0, + "keep_closed": 0, + "label": "Role Permissions", + "link_to": "permission-manager", + "link_type": "Page", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "mail", + "indent": 0, + "keep_closed": 0, + "label": "Email Account", + "link_to": "Email Account", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, + "title": "Organization", + "type": "Workspace" +} diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7f524c82912..a3a1884cae2 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -426,6 +426,7 @@ class DeliveryNote(SellingController): "stock_delivered_but_not_billed", "disable_sdbnb_in_sr", "default_expense_account", + "enable_stock_delivered_but_not_billed", ], as_dict=True, ) @@ -433,7 +434,7 @@ class DeliveryNote(SellingController): sdbnb_account = company_values.stock_delivered_but_not_billed disable_sdbnb_in_sr = company_values.disable_sdbnb_in_sr default_expense_account = company_values.default_expense_account - + is_enabled_sdbnb = company_values.enable_stock_delivered_but_not_billed for item in self.items: if item.get("against_sales_invoice"): if sdbnb_account and item.expense_account == sdbnb_account: @@ -447,14 +448,16 @@ class DeliveryNote(SellingController): # Only stock items if is_stock_item and not item.get("is_fixed_asset") and not item.get("is_subcontracted"): # Sales Return handling - if self.is_return and disable_sdbnb_in_sr: + if self.is_return and disable_sdbnb_in_sr and sdbnb_account and is_enabled_sdbnb: if default_expense_account and ( not item.expense_account or item.expense_account == sdbnb_account ): item.expense_account = default_expense_account - elif sdbnb_account: + elif sdbnb_account and is_enabled_sdbnb: item.expense_account = sdbnb_account + elif sdbnb_account and item.expense_account == sdbnb_account: + item.expense_account = default_expense_account if not item.expense_account and default_expense_account: item.expense_account = default_expense_account diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 971a2555b2c..c5db9cdcecb 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -50,7 +50,7 @@ class TestDeliveryNote(ERPNextTestSuite): self.load_test_records("Stock Entry") def get_perpetual_defaults(self): - company = frappe.get_doc("Company", "_Test Company with perpetual inventory") + company = frappe.get_doc("Company", "_Test SDBNB Company") self.perpetual_company = company.name self.perpetual_account = company.stock_delivered_but_not_billed self.perpetual_cost_center = company.cost_center diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index ed6d4efe43d..5eb7f07f4bd 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -1074,62 +1074,141 @@ $.extend(erpnext.item, { function make_fields_from_attribute_values(attr_dict) { let fields = []; - let att_key = frm.doc.attributes.map((idx) => idx.attribute); - att_key.forEach((name, i) => { + let attributes = frm.doc.attributes.filter((row) => !row.disabled); + attributes.forEach((row, i) => { + let name = row.attribute; if (i % 3 === 0) { fields.push({ fieldtype: "Section Break" }); } - fields.push({ fieldtype: "Column Break", label: name }); + fields.push({ fieldtype: "Column Break" }); fields.push({ - fieldtype: "Data", - placeholder: "Search", - fieldname: `search_${frappe.scrub(name)}`, - onchange: function (e) { - let value = e.target.value; - let result = attr_dict[name].filter((attr_value) => - attr_value.toString().toLowerCase().includes(value.toLowerCase()) - ); - attr_dict[name].forEach((attr_value) => { - if (result.includes(attr_value)) { - me.multiple_variant_dialog.set_df_property(attr_value, "hidden", 0); - } else { - me.multiple_variant_dialog.set_df_property(attr_value, "hidden", 1); - } - }); - }, - }); - attr_dict[name].forEach((value) => { - fields.push({ - fieldtype: "Check", - label: value, - fieldname: value, - default: 0, - onchange: function () { - let selected_attributes = get_selected_attributes(); - let lengths = Object.keys(selected_attributes).map((key) => { - return selected_attributes[key].length; - }); - if (!lengths.length) { - me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants")); - me.multiple_variant_dialog.disable_primary_action(); - } else { - let no_of_combinations = lengths.reduce((a, b) => a * b, 1); - let msg; - if (no_of_combinations === 1) { - msg = __("Make {0} Variant", [no_of_combinations]); - } else { - msg = __("Make {0} Variants", [no_of_combinations]); - } - me.multiple_variant_dialog.get_primary_btn().html(msg); - me.multiple_variant_dialog.enable_primary_action(); - } - }, - }); + fieldtype: "MultiSelectPills", + label: name, + fieldname: frappe.scrub(name), + placeholder: __("Search values..."), + get_data: (txt) => get_attribute_suggestions(attr_dict[name], txt), + onchange: update_primary_action, }); }); return fields; } + function get_attribute_suggestions(spec, txt) { + if (!spec) return []; + return Array.isArray(spec) ? filter_list(spec, txt) : numeric_suggestions(spec, txt); + } + + // Cap matches so a long value list never hands everything to Awesomplete, + // which would freeze the browser. + function filter_list(values, txt) { + txt = (txt || "").toLowerCase(); + let matches = []; + for (let value of values) { + if (!txt || value.toLowerCase().includes(txt)) { + matches.push(value); + if (matches.length >= 50) break; + } + } + return matches; + } + + // Numeric ranges aren't enumerated. With no input, preview the first few + // values; once the user types, accept it only if it lies on the increment + // within [from, to]. Both paths are cheap even for huge ranges. + function numeric_suggestions(range, txt) { + let { from_range: from, to_range: to, increment } = range; + if (!(increment > 0) || from > to) return []; + + txt = (txt || "").trim(); + if (!txt) { + let preview = []; + for ( + let value = from; + value <= to && preview.length < 50; + value = flt(value + increment, 6) + ) { + preview.push(String(value)); + } + return preview; + } + + return is_valid_attribute_value(range, txt) ? [String(flt(txt, 6))] : []; + } + + function is_valid_attribute_value(spec, value) { + if (!spec || !value) return false; + if (Array.isArray(spec)) return spec.includes(value); + + let { from_range: from, to_range: to, increment } = spec; + if (!(increment > 0)) return false; + + // Reject anything that isn't cleanly a number ("abc", "5000xyz", ""); + // flt would coerce these to 0 and wrongly accept them. + let text = String(value).trim(); + let num = Number(text); + if (text === "" || !Number.isFinite(num)) return false; + + if (num < from || num > to) return false; + let steps = (num - from) / increment; + return Math.abs(Math.round(steps) - steps) <= 1e-6; + } + + // Block variant creation if anything is wrong: an invalid committed pill, or + // text typed but not added as a pill (which get_selected_attributes would + // otherwise drop silently). The user must fix each before creation proceeds. + function validate_selected_attributes() { + let errors = []; + frm.doc.attributes.forEach((row) => { + if (row.disabled) return; + let field = me.multiple_variant_dialog.get_field(frappe.scrub(row.attribute)); + if (!field) return; + + let attribute = frappe.utils.escape_html(row.attribute); + let spec = attr_val_fields[row.attribute]; + + let invalid = [ + ...new Set((field.get_value() || []).filter((v) => !is_valid_attribute_value(spec, v))), + ]; + if (invalid.length) { + let values = invalid.map(frappe.utils.escape_html).join(", "); + errors.push(__("{0}: remove invalid value(s) {1}", [attribute, values])); + } + + let pending = (field.$input?.val() || "").trim(); + if (pending) { + let value = frappe.utils.escape_html(pending); + errors.push( + __("{0}: select the typed value {1} from the list or clear it", [attribute, value]) + ); + } + }); + + if (errors.length) { + frappe.throw({ + title: __("Invalid Attribute Values"), + message: errors.join("
"), + indicator: "red", + }); + } + } + + function update_primary_action() { + let selected_attributes = get_selected_attributes(); + let counts = Object.keys(selected_attributes).map((key) => selected_attributes[key].length); + if (!counts.length) { + me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants")); + me.multiple_variant_dialog.disable_primary_action(); + } else { + let no_of_combinations = counts.reduce((a, b) => a * b, 1); + let msg = + no_of_combinations === 1 + ? __("Make {0} Variant", [no_of_combinations]) + : __("Make {0} Variants", [no_of_combinations]); + me.multiple_variant_dialog.get_primary_btn().html(msg); + me.multiple_variant_dialog.enable_primary_action(); + } + } + function make_and_show_dialog(fields) { me.multiple_variant_dialog = new frappe.ui.Dialog({ title: __("Select Attribute Values"), @@ -1155,6 +1234,8 @@ $.extend(erpnext.item, { }); me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => { + validate_selected_attributes(); + let selected_attributes = get_selected_attributes(); let use_template_image = me.multiple_variant_dialog.get_value("use_template_image"); @@ -1182,72 +1263,70 @@ $.extend(erpnext.item, { }); }); - $($(me.multiple_variant_dialog.$wrapper.find(".form-column")).find(".frappe-control")).css( - "margin-bottom", - "0px" - ); - me.multiple_variant_dialog.disable_primary_action(); me.multiple_variant_dialog.clear(); me.multiple_variant_dialog.show(); - me.multiple_variant_dialog.$wrapper - .find("div[data-fieldname^='search_']") - .find(".clearfix") - .hide(); } function get_selected_attributes() { let selected_attributes = {}; - me.multiple_variant_dialog.$wrapper.find(".form-column").each((i, col) => { - if (i === 0) return; - let attribute_name = $(col).find(".column-label").html().trim(); - selected_attributes[attribute_name] = []; - let checked_opts = $(col).find(".checkbox input"); - checked_opts.each((i, opt) => { - if ($(opt).is(":checked")) { - selected_attributes[attribute_name].push($(opt).attr("data-fieldname")); - } - }); - if (!selected_attributes[attribute_name].length) { - delete selected_attributes[attribute_name]; + frm.doc.attributes.forEach((row) => { + if (row.disabled) return; + let values = me.multiple_variant_dialog.get_value(frappe.scrub(row.attribute)); + if (values && values.length) { + selected_attributes[row.attribute] = values; } }); - return selected_attributes; } frm.doc.attributes.forEach(function (d) { if (!d.disabled) { let p = new Promise((resolve) => { - if (!d.numeric_values) { - frappe - .call({ - method: "frappe.client.get_list", - args: { - doctype: "Item Attribute Value", - filters: [["parent", "=", d.attribute]], - fields: ["attribute_value"], - limit_page_length: 0, - parent: "Item Attribute", - order_by: "idx", - }, - }) - .then((r) => { - if (r.message) { - attr_val_fields[d.attribute] = r.message.map(function (d) { - return d.attribute_value; + // Read the numeric configuration from the Item Attribute master + // instead of the variant attribute row, which may be stale or + // blank if the attribute was made numeric after it was added here. + frappe.db + .get_value("Item Attribute", d.attribute, [ + "numeric_values", + "from_range", + "to_range", + "increment", + ]) + .then((res) => { + let attr = res.message || {}; + + if (!attr.numeric_values) { + frappe + .call({ + method: "frappe.client.get_list", + args: { + doctype: "Item Attribute Value", + filters: [["parent", "=", d.attribute]], + fields: ["attribute_value"], + limit_page_length: 0, + parent: "Item Attribute", + order_by: "idx", + }, + }) + .then((r) => { + attr_val_fields[d.attribute] = (r.message || []).map( + (row) => row.attribute_value + ); + resolve(); }); - resolve(); - } - }); - } else { - let values = []; - for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { - values.push(i); - } - attr_val_fields[d.attribute] = values; - resolve(); - } + } else { + // Store the range instead of enumerating it; a large range + // (e.g. 1-100000) is slow to build and to search. Values are + // validated against the range on demand while typing. + attr_val_fields[d.attribute] = { + from_range: flt(attr.from_range), + to_range: flt(attr.to_range), + increment: flt(attr.increment), + }; + resolve(); + } + }); }); promises.push(p); diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index a26f58430bf..3fff0cb1c28 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -239,6 +239,7 @@ class Item(Document): self.validate_item_defaults() self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() + self.validate_serialized_change_with_bundle() self.validate_standard_cost_change() self.validate_item_tax_net_rate_range() @@ -1130,6 +1131,25 @@ class Item(Document): frappe.throw(msg, title=_("Linked with submitted documents")) + def validate_serialized_change_with_bundle(self): + """Block turning a serialized item non-serialized while any Serial and Batch Bundle still exists + for it. Such bundles carry the item's serial numbers; the user must delete or cancel them first.""" + if self.is_new() or self.has_serial_no or not self._doc_before_save: + return + + # Only relevant when the item was serialized before and is now being unset. + if not self._doc_before_save.has_serial_no: + return + + # Draft (docstatus 0) or submitted (docstatus 1) bundles block the change; cancelled ones don't. + if frappe.db.count("Serial and Batch Bundle", {"item_code": self.name, "docstatus": ("<", 2)}): + frappe.throw( + _( + "Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first." + ).format(frappe.bold(self.name)), + title=_("Serial and Batch Bundle Exists"), + ) + def _get_linked_submitted_documents(self, changed_fields: list[str]) -> dict[str, str] | None: linked_doctypes = [ "Delivery Note Item", @@ -1372,7 +1392,8 @@ def get_purchase_voucher_details(doctype, item_code, document_name=None): query = query.select(parent_doc.transaction_date) query = query.orderby(parent_doc.transaction_date, parent_doc.name, order=Order.desc) - return query.run(as_dict=1) + # only the latest ([0]) row is ever used, so fetch just that instead of every purchase of the item + return query.limit(1).run(as_dict=1) def check_stock_uom_with_bin(item, stock_uom): @@ -1762,3 +1783,13 @@ def get_default_warehouse_for_opening_stock(item, company: str, warehouse: str | "No warehouse found for company {0}. Please set a Default Warehouse in Item Defaults or Stock Settings." ).format(frappe.bold(company)) ) + + +def on_doctype_update(): + if frappe.db.db_type == "postgres": + # The Item link-search (erpnext.controllers.queries.item_query) filters + # `item_code/item_name LIKE '%txt%'` -- a leading-wildcard LIKE no btree can serve. pg_trgm + # GIN indexes accelerate it. Item is read-heavy/write-light master data, so GIN maintenance + # cost is negligible. Postgres-only (`using` is a no-op on MariaDB, which has its own FULLTEXT). + frappe.db.add_index("Item", ["item_code"], using="gin_trgm") + frappe.db.add_index("Item", ["item_name"], using="gin_trgm") diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 06cc5c33f94..7569d1c538e 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -1120,6 +1120,47 @@ class TestItem(ERPNextTestSuite): sabb_qty = frappe.db.get_value("Serial and Batch Bundle", serial_and_batch_bundle, "total_qty") self.assertEqual(abs(sabb_qty), properties["opening_stock"]) + def test_cannot_unset_serialized_while_bundle_exists(self): + from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + make_serial_batch_bundle, + ) + + item = make_item( + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TSN-UNSET-.####"} + ).name + + serial_no = f"{item}-SN-01" + frappe.get_doc( + {"doctype": "Serial No", "serial_no": serial_no, "item_code": item, "company": "_Test Company"} + ).insert() + + # A draft (unsubmitted) Serial and Batch Bundle for the item must block the change. + bundle = make_serial_batch_bundle( + { + "item_code": item, + "warehouse": "_Test Warehouse - _TC", + "company": "_Test Company", + "qty": 1, + "rate": 100, + "voucher_type": "Stock Entry", + "serial_nos": [serial_no], + "type_of_transaction": "Inward", + "do_not_submit": True, + "ignore_sabb_validation": True, + } + ) + + doc = frappe.get_doc("Item", item) + doc.has_serial_no = 0 + self.assertRaises(frappe.ValidationError, doc.save) + + # Once the bundle is removed, the item can be made non-serialized. + frappe.delete_doc("Serial and Batch Bundle", bundle.name, force=True) + doc = frappe.get_doc("Item", item) + doc.has_serial_no = 0 + doc.save() + self.assertEqual(frappe.db.get_value("Item", item, "has_serial_no"), 0) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.js b/erpnext/stock/doctype/item_attribute/item_attribute.js index 22c7978ac3c..d6f0f259174 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.js +++ b/erpnext/stock/doctype/item_attribute/item_attribute.js @@ -1,4 +1,13 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Item Attribute", {}); +frappe.ui.form.on("Item Attribute", { + numeric_values(frm) { + // Numeric attributes have no discrete values; drop the rows so their + // mandatory Attribute Value / Abbreviation don't block the save. + if (frm.doc.numeric_values) { + frm.clear_table("item_attribute_values"); + frm.refresh_field("item_attribute_values"); + } + }, +}); diff --git a/erpnext/stock/doctype/pick_list/mapper.py b/erpnext/stock/doctype/pick_list/mapper.py index f9f5fdc8e08..df2c3f4e2c2 100644 --- a/erpnext/stock/doctype/pick_list/mapper.py +++ b/erpnext/stock/doctype/pick_list/mapper.py @@ -285,9 +285,6 @@ def create_stock_entry(pick_list: str | dict): pick_list = frappe.get_doc(frappe.parse_json(pick_list)) validate_item_locations(pick_list) - if stock_entry_exists(pick_list.get("name")): - return frappe.msgprint(_("Stock Entry has already been created against this Pick List")) - stock_entry = frappe.new_doc("Stock Entry") stock_entry.pick_list = pick_list.get("name") stock_entry.purpose = pick_list.get("purpose") @@ -301,6 +298,9 @@ def create_stock_entry(pick_list: str | dict): else: stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry) + if not stock_entry.get("items"): + return frappe.msgprint(_("All picked items have already been transferred against this Pick List")) + stock_entry.set_missing_values() return stock_entry.as_dict() @@ -366,6 +366,8 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): stock_entry.project = work_order.project for location in pick_list.locations: + if get_pending_transfer_stock_qty(location) <= 0: + continue item = frappe._dict() update_common_item_properties(item, location) item.t_warehouse = wip_warehouse @@ -377,6 +379,8 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): def update_stock_entry_based_on_material_request(pick_list, stock_entry): for location in pick_list.locations: + if get_pending_transfer_stock_qty(location) <= 0: + continue target_warehouse = None if location.material_request_item: target_warehouse = frappe.get_value( @@ -392,6 +396,8 @@ def update_stock_entry_based_on_material_request(pick_list, stock_entry): def update_stock_entry_items_with_no_reference(pick_list, stock_entry): for location in pick_list.locations: + if get_pending_transfer_stock_qty(location) <= 0: + continue item = frappe._dict() update_common_item_properties(item, location) @@ -400,11 +406,18 @@ def update_stock_entry_items_with_no_reference(pick_list, stock_entry): return stock_entry +def get_pending_transfer_stock_qty(location): + """Stock qty of this pick list row still to be moved into a Stock Entry.""" + return flt(location.picked_qty) - flt(location.transferred_qty) + + def update_common_item_properties(item, location): + pending_stock_qty = get_pending_transfer_stock_qty(location) item.item_code = location.item_code + item.item_name = location.item_name item.s_warehouse = location.warehouse - item.transfer_qty = location.picked_qty - item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty")) + item.transfer_qty = pending_stock_qty + item.qty = flt(pending_stock_qty / (location.conversion_factor or 1), location.precision("qty")) item.uom = location.uom item.conversion_factor = location.conversion_factor item.stock_uom = location.stock_uom @@ -412,3 +425,4 @@ def update_common_item_properties(item, location): item.serial_no = location.serial_no item.batch_no = location.batch_no item.material_request_item = location.material_request_item + item.pick_list_item = location.name diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 9ee1b7a1922..55e66f74b3c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -190,7 +190,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled", + "options": "Draft\nOpen\nPartly Delivered\nPartially Transferred\nCompleted\nCancelled", "print_hide": 1, "read_only": 1, "report_hide": 1, @@ -278,7 +278,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2026-02-06 18:14:18.361039", + "modified": "2026-07-01 14:27:50.617011", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index a25770351e4..846c020de72 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -71,7 +71,9 @@ class PickList(TransactionBase): purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"] scan_barcode: DF.Data | None scan_mode: DF.Check - status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"] + status: DF.Literal[ + "Draft", "Open", "Partly Delivered", "Partially Transferred", "Completed", "Cancelled" + ] work_order: DF.Link | None # end: auto-generated types @@ -417,6 +419,34 @@ class PickList(TransactionBase): return stock_entry_exists(self.name) + def get_transfer_status(self): + """Return the pick list's transfer progress based on how much of the picked qty has been + moved into submitted Stock Entries (tracked on Pick List Item.transferred_qty). + + Only applies to purposes that move stock via Stock Entry; the Delivery purpose is tracked + via delivery_status instead. Returns "Completed", "Partially Transferred" or None.""" + if self.purpose == "Delivery": + return None + + total_picked = sum(flt(row.picked_qty) for row in self.locations) + if not total_picked: + return None + + total_transferred = sum(flt(row.transferred_qty) for row in self.locations) + if total_transferred <= 0: + return None + + if total_transferred >= total_picked: + return "Completed" + + return "Partially Transferred" + + def is_fully_transferred(self): + return self.get_transfer_status() == "Completed" + + def is_partially_transferred(self): + return self.get_transfer_status() == "Partially Transferred" + def update_reference_qty(self): packed_items = [] so_items = [] diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js index a675c95f973..5bc4f2f3eef 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -7,6 +7,7 @@ frappe.listview_settings["Pick List"] = { Draft: "red", Open: "orange", "Partly Delivered": "orange", + "Partially Transferred": "yellow", Completed: "green", Cancelled: "red", }; diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index c442539f1f5..e37cb0a3532 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -13,6 +13,7 @@ from erpnext.stock.doctype.pick_list.mapper import ( create_delivery, create_delivery_note, create_dn_for_pick_lists, + create_stock_entry, ) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( @@ -1221,6 +1222,64 @@ class TestPickList(ERPNextTestSuite): pl.reload() self.assertEqual(pl.status, "Cancelled") + def test_pick_list_partial_transfer_status(self): + """Partial Stock Entries from a Pick List should track transferred_qty and drive the + Partially Transferred / Completed status, and allow further transfers for the remainder.""" + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + item = make_item(properties={"is_stock_item": 1}).name + source_warehouse = "_Test Warehouse - _TC" + target_warehouse = create_warehouse("_Test Transfer Target Warehouse") + make_stock_entry(item=item, to_warehouse=source_warehouse, qty=10) + + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "pick_manually": 1, + "locations": [ + { + "item_code": item, + "qty": 10, + "stock_qty": 10, + "conversion_factor": 1, + "warehouse": source_warehouse, + "picked_qty": 10, + } + ], + } + ) + pick_list.submit() + self.assertEqual(pick_list.status, "Open") + + # Transfer 4 of the 10 picked units. + se1 = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertEqual(se1.items[0].qty, 10) + se1.items[0].qty = 4 + se1.items[0].t_warehouse = target_warehouse + se1.submit() + + pick_list.reload() + self.assertEqual(pick_list.locations[0].transferred_qty, 4) + self.assertEqual(pick_list.status, "Partially Transferred") + + # The next Stock Entry should only offer the remaining 6 units. + se2 = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertEqual(se2.items[0].qty, 6) + se2.items[0].t_warehouse = target_warehouse + se2.submit() + + pick_list.reload() + self.assertEqual(pick_list.locations[0].transferred_qty, 10) + self.assertEqual(pick_list.status, "Completed") + + # Cancelling the last entry rolls transferred_qty and status back. + se2.cancel() + pick_list.reload() + self.assertEqual(pick_list.locations[0].transferred_qty, 4) + self.assertEqual(pick_list.status, "Partially Transferred") + def test_pick_list_validation(self): warehouse = "_Test Warehouse - _TC" item = make_item("Test Non Serialized Pick List Item", properties={"is_stock_item": 1}).name diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 01630278168..658dff42d7f 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -22,6 +22,7 @@ "conversion_factor", "stock_uom", "delivered_qty", + "transferred_qty", "available_quantity_section", "actual_qty", "column_break_kyek", @@ -255,6 +256,16 @@ "read_only": 1, "report_hide": 1 }, + { + "default": "0", + "fieldname": "transferred_qty", + "fieldtype": "Float", + "label": "Transferred Qty (in Stock UOM)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, { "fieldname": "available_quantity_section", "fieldtype": "Section Break", @@ -285,7 +296,7 @@ ], "istable": 1, "links": [], - "modified": "2026-03-17 16:25:10.358013", + "modified": "2026-07-01 14:27:50.617011", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py index bdba97f4056..97e6525c97b 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py @@ -39,6 +39,7 @@ class PickListItem(Document): stock_qty: DF.Float stock_reserved_qty: DF.Float stock_uom: DF.Link | None + transferred_qty: DF.Float uom: DF.Link | None use_serial_batch_fields: DF.Check warehouse: DF.Link | None diff --git a/erpnext/stock/doctype/purchase_receipt/services/reservation.py b/erpnext/stock/doctype/purchase_receipt/services/reservation.py index 8141ebb9ee5..6be9734d4f1 100644 --- a/erpnext/stock/doctype/purchase_receipt/services/reservation.py +++ b/erpnext/stock/doctype/purchase_receipt/services/reservation.py @@ -63,6 +63,13 @@ class PurchaseReceiptStockReservation: return production_plan_references = self.get_production_plan_references() + if not production_plan_references: + return + + reservable_plans = self.get_reservable_production_plans(production_plan_references) + if not reservable_plans: + return + production_plan_items = [] doc.reload() @@ -70,6 +77,9 @@ class PurchaseReceiptStockReservation: for row in doc.items: if row.material_request_item and row.material_request_item in production_plan_references: _ref = production_plan_references[row.material_request_item] + if _ref.production_plan not in reservable_plans: + continue + docnames.append(_ref.production_plan) row.update( { @@ -95,6 +105,25 @@ class PurchaseReceiptStockReservation: docnames, from_doctype="Production Plan", to_doctype="Work Order" ) + def get_reservable_production_plans(self, production_plan_references: frappe._dict) -> set: + """Production Plans that opted into stock reservation (``reserve_stock``). + + A Production Plan only gets this flag set if "Auto Reserve Stock" was enabled in + Stock Settings when it was created, or the user ticked "Reserve Stock" manually. + Without this check, a Purchase Receipt would auto-reserve stock for every + Production Plan whenever "Enable Stock Reservation" is on, ignoring both of those. + """ + plan_names = {ref.production_plan for ref in production_plan_references.values()} + return { + p.name + for p in frappe.get_all( + "Production Plan", + filters={"name": ["in", list(plan_names)]}, + fields=["name", "reserve_stock"], + ) + if p.reserve_stock + } + def get_production_plan_references(self) -> frappe._dict: production_plan_references = frappe._dict() material_request_items = [] diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index b9bb3d931da..2d94a892aeb 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -425,10 +425,10 @@ def repost(doc): if isinstance(message, dict): message = message.get("message") - status = "Failed" - # If failed because of timeout, set status to In Progress - if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback): - status = "In Progress" + # Recoverable errors (deadlock, lock/query timeout, job timeout) re-queue as In Progress. + # Classify by type: the old traceback string-match only knew MariaDB's "Deadlock found" and + # missed Postgres deadlocks ("deadlock detected"), failing them permanently. + status = "In Progress" if isinstance(e, RecoverableErrors) else "Failed" if traceback: message += "

" + "Traceback:
" + traceback @@ -447,7 +447,8 @@ def repost(doc): "Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" ) - if outgoing_email_account and not isinstance(e, RecoverableErrors): + # status == "Failed" already implies e is not recoverable, so no need to re-check here. + if outgoing_email_account: notify_error_to_stock_managers(doc, message) doc.set_status("Failed") finally: @@ -510,7 +511,7 @@ def repost_gl_entries(doc): transactions = directly_dependent_transactions + list(repost_affected_transaction) # handle stock delivered but not billed ledger entries - if frappe.get_cached_value("Company", doc.company, "stock_delivered_but_not_billed"): + if frappe.get_cached_value("Company", doc.company, "enable_stock_delivered_but_not_billed"): _update_post_delivery_billed_vouchers(transactions) enable_separate_reposting_for_gl = frappe.db.get_single_value( diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 40df17a061d..d2c5eaa6096 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -220,6 +220,39 @@ class TestRepostItemValuation(ERPNextTestSuite, StockTestMixin): sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))), ) + def test_recoverable_error_requeues_instead_of_failing(self): + # A recoverable DB error (e.g. Postgres deadlock -> QueryDeadlockError) must re-queue the + # repost as "In Progress"; a non-recoverable error still fails. Regression: the old check + # string-matched MariaDB's "Deadlock found" and missed Postgres deadlocks ("deadlock detected"). + from unittest.mock import patch + + from frappe.exceptions import QueryDeadlockError + + from erpnext.stock.doctype.repost_item_valuation import repost_item_valuation as riv + + orig_max_writes = frappe.db.MAX_WRITES_PER_TRANSACTION + self.addCleanup(setattr, frappe.db, "MAX_WRITES_PER_TRANSACTION", orig_max_writes) + + def status_after(error): + doc = frappe.new_doc("Repost Item Valuation") + doc.name = "test-recoverable-riv" + doc.set_status = doc.log_error = doc.db_set = MagicMock() + captured = {} + with ( + patch.object(frappe, "in_test", False), + patch.object(frappe.db, "exists", return_value=True), + patch.object(frappe.db, "commit"), + patch.object(frappe.db, "rollback"), + patch.object(frappe.db, "set_value", side_effect=lambda *a, **k: captured.update(a[2])), + patch.object(riv, "repost_sl_entries", side_effect=error), + patch.object(frappe, "get_cached_value", return_value=None), + ): + riv.repost(doc) + return captured.get("status") + + self.assertEqual(status_after(QueryDeadlockError("deadlock detected")), "In Progress") + self.assertEqual(status_after(ValueError("boom")), "Failed") + def test_gl_repost_progress(self): from erpnext.accounts import utils diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 747b43ca53f..09e6c87a58b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1814,6 +1814,27 @@ class SerialandBatchBundle(Document): self.set("entries", []) +def on_doctype_update(): + if frappe.db.db_type == "postgres": + # Bundle-direct lookups (get_ledgers_from_serial_batch_bundle, get_picked_*) always filter + # `is_cancelled = 0` and scope by voucher_no or item_code+warehouse -- none of which the parent + # bundle is otherwise indexed on (only voucher_type/voucher_detail_no are). Partial indexes keep + # only the active bundles. Postgres-only (`where` is a no-op on MariaDB, and MariaDB's optimizer + # ignores partial predicates anyway). + frappe.db.add_index( + "Serial and Batch Bundle", + ["voucher_no"], + index_name="sabb_active_voucher", + where="is_cancelled = 0", + ) + frappe.db.add_index( + "Serial and Batch Bundle", + ["item_code", "warehouse"], + index_name="sabb_active_item_wh", + where="is_cancelled = 0", + ) + + @frappe.whitelist() def download_blank_csv_template(content: str | list): csv_data = [] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index ccb096d29ea..ca72ced6157 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -164,6 +164,15 @@ class StockEntry(StockController, SubcontractingInwardController): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._configure_purpose_class() + self.status_updater = [ + { + "source_dt": "Stock Entry Detail", + "target_dt": "Pick List Item", + "join_field": "pick_list_item", + "target_field": "transferred_qty", + "source_field": "transfer_qty", + } + ] if self.subcontracting_inward_order: self.subcontract_data = frappe._dict( @@ -349,6 +358,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.delink_asset_repair_sabb() self.validate_closed_subcontracting_order() self.update_subcontracting_order_status() + self.update_pick_list_status() self.cancel_stock_reserve_for_wip_and_fg() if self.work_order and self.purpose == "Material Consumption for Manufacture": @@ -1484,6 +1494,9 @@ class StockEntry(StockController, SubcontractingInwardController): def update_pick_list_status(self): from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + if self.pick_list: + self.update_qty() + update_pick_list_status(self.pick_list) def set_missing_values(self): diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index ea9d3b75b51..75f45275de1 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -72,6 +72,7 @@ "col_break6", "material_request", "material_request_item", + "pick_list_item", "original_item", "reference_section", "against_stock_entry", @@ -424,6 +425,16 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "pick_list_item", + "fieldtype": "Link", + "hidden": 1, + "label": "Pick List Item", + "no_copy": 1, + "options": "Pick List Item", + "print_hide": 1, + "read_only": 1 + }, { "fieldname": "original_item", "fieldtype": "Link", @@ -679,7 +690,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-06-30 12:18:34.132425", + "modified": "2026-07-01 14:27:50.617011", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py index 3224ea905c7..4e690d4d8ec 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py @@ -58,6 +58,7 @@ class StockEntryDetail(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + pick_list_item: DF.Link | None po_detail: DF.Data | None project: DF.Link | None putaway_rule: DF.Link | None 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 8ec74a3df4d..f20f078f0f3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -364,3 +364,15 @@ class StockLedgerEntry(Document): def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["item_code", "warehouse", "posting_datetime", "creation"]) + + if frappe.db.db_type == "postgres": + # Postgres-only partial index for date-range stock reports (Stock Ledger / Stock Balance) + # that scan across all items: they filter `is_cancelled = 0` and sort by posting_datetime. + # The existing item_code-leading composite can't serve an all-items date scan. `where` is a + # no-op on MariaDB, so this is added only on postgres. + frappe.db.add_index( + "Stock Ledger Entry", + ["company", "posting_datetime", "creation"], + index_name="sle_active_posting", + where="is_cancelled = 0", + ) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index be563e9941a..5a17d322288 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -455,6 +455,7 @@ def get_basic_details(ctx: frappe._dict, item, overwrite_warehouse=True) -> frap [ "stock_delivered_but_not_billed", "disable_sdbnb_in_sr", + "enable_stock_delivered_but_not_billed", ], as_dict=True, ) @@ -464,6 +465,7 @@ def get_basic_details(ctx: frappe._dict, item, overwrite_warehouse=True) -> frap and ctx.is_stock_item and company_values and company_values.stock_delivered_but_not_billed + and company_values.enable_stock_delivered_but_not_billed and not ctx.get("is_fixed_asset") and not ctx.get("is_subcontracted") ): @@ -1220,8 +1222,6 @@ def get_item_price(pctx: frappe._dict, item_code, ignore_party=False, force_batc optional fields transaction_date, customer, supplier :param item_code: str, Item Doctype field item_code """ - pctx: frappe._dict = frappe._dict(pctx) - ip = frappe.qb.DocType("Item Price") query = ( frappe.qb.from_(ip) diff --git a/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py new file mode 100644 index 00000000000..8e2004c7d46 --- /dev/null +++ b/erpnext/stock/report/cogs_by_item_group/test_cogs_by_item_group.py @@ -0,0 +1,78 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.cogs_by_item_group.cogs_by_item_group import execute +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company with perpetual inventory" + + +class TestCogsByItemGroup(ERPNextTestSuite): + def run_report(self, **extra) -> list: + filters = frappe._dict( + company=COMPANY, + from_date="2026-01-01", + to_date="2026-12-31", + ) + filters.update(extra) + return execute(filters)[1] + + def test_cogs_for_item_group(self): + # A dedicated item group with a single item keeps `agg_value` scoped to this + # test's COGS. The report sums COGS up the whole item-group tree keyed on the + # company's default expense account, so a shared group would accumulate COGS + # booked by any other test/fixture for the same company within the date range. + # The group name is unique per run so items created by earlier runs (which + # reuse a fixed group name) can't inflate the total either. + item_group = make_item_group(f"_Test COGS Item Group {frappe.generate_hash(length=6)}") + item = make_item(properties={"is_stock_item": 1, "item_group": item_group}).name + + make_stock_entry( + item_code=item, + to_warehouse="Stores - TCP1", + qty=10, + rate=100, + company=COMPANY, + posting_date="2026-06-01", + ) + + # A Sales Invoice with update_stock delivers the goods and books the COGS + # against the company's default expense account, which the report keys on. + create_sales_invoice( + item_code=item, + qty=4, + rate=150, + warehouse="Stores - TCP1", + company=COMPANY, + update_stock=1, + cost_center="Main - TCP1", + parent_cost_center="Main - TCP1", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + posting_date="2026-06-02", + ) + + data = self.run_report() + rows = [row for row in data if item_group in row.get("item_group")] + self.assertTrue(rows, "No row found for the dedicated item group") + # 4 units delivered at 100 valuation rate -> 400 COGS. + self.assertEqual(rows[0].get("cogs_debit"), 400) + + +def make_item_group(name: str) -> str: + if not frappe.db.exists("Item Group", name): + frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": name, + "parent_item_group": "All Item Groups", + "is_group": 0, + } + ).insert() + return name diff --git a/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py new file mode 100644 index 00000000000..f5f62c5bde2 --- /dev/null +++ b/erpnext/stock/report/delivery_note_trends/test_delivery_note_trends.py @@ -0,0 +1,126 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.delivery_note_trends.delivery_note_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +ITEM = "_Test Item" +WAREHOUSE = "Stores - _TC" +CUSTOMER = "_Test Customer" + + +class TestDeliveryNoteTrends(ERPNextTestSuite): + def run_report_full(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": "_Test Fiscal Year 2026", + "period": "Yearly", + "based_on": "Item", + "group_by": "", + } + ) + filters.update(extra) + columns, data = execute(filters)[:2] + return columns, data + + # trend columns are "Label:fieldtype:width" strings; assert by label so the index + # stays correct across period / based_on / group_by combinations. + @staticmethod + def labels(columns): + return [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + + def find_row(self, columns, data, match): + labels = self.labels(columns) + for row in data: + if all(row[labels.index(label)] == value for label, value in match.items()): + return row + return None + + def value(self, columns, row, label): + if not row: + return 0 + return row[self.labels(columns).index(label)] or 0 + + def values(self, match, wanted_labels, **extra): + columns, data = self.run_report_full(**extra) + row = self.find_row(columns, data, match) + return {label: self.value(columns, row, label) for label in wanted_labels} + + def deliver(self, qty=5, rate=200, customer=CUSTOMER, posting_date="2026-06-01"): + # stock the item first so the delivery note can ship, then deliver + make_stock_entry( + item_code=ITEM, to_warehouse=WAREHOUSE, qty=qty + 10, rate=100, posting_date=posting_date + ) + create_delivery_note( + item_code=ITEM, + warehouse=WAREHOUSE, + qty=qty, + rate=rate, + customer=customer, + company="_Test Company", + posting_date=posting_date, + ) + + def test_delivery_qty_in_trend(self): + # A Delivery Note of qty 5 @ rate 200 sums to qty 5 / amount 1000 (base_net_amount) + # in the yearly bucket and the Total columns. + cols = ["_Test Fiscal Year 2026 (Qty)", "_Test Fiscal Year 2026 (Amt)", "Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols) + self.deliver() + after = self.values({"Item": ITEM}, cols) + self.assertEqual(after[cols[0]] - before[cols[0]], 5) + self.assertEqual(after[cols[1]] - before[cols[1]], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_monthly_period_buckets(self): + cols = ["Jun (Qty)", "Jun (Amt)", "Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, period="Monthly") + self.deliver(posting_date="2026-06-01") + after = self.values({"Item": ITEM}, cols, period="Monthly") + # the June delivery lands only in the June bucket, and rolls up into the Total columns + self.assertEqual(after["Jun (Qty)"] - before["Jun (Qty)"], 5) + self.assertEqual(after["Jun (Amt)"] - before["Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_quarterly_period_buckets(self): + # 2026-06-01 falls in the Apr-Jun quarter + cols = ["Apr-Jun (Qty)", "Apr-Jun (Amt)", "Total(Qty)"] + before = self.values({"Item": ITEM}, cols, period="Quarterly") + self.deliver(posting_date="2026-06-01") + after = self.values({"Item": ITEM}, cols, period="Quarterly") + self.assertEqual(after["Apr-Jun (Qty)"] - before["Apr-Jun (Qty)"], 5) + self.assertEqual(after["Apr-Jun (Amt)"] - before["Apr-Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + + def test_based_on_customer(self): + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Customer": CUSTOMER}, cols, based_on="Customer") + self.deliver(customer=CUSTOMER) + after = self.values({"Customer": CUSTOMER}, cols, based_on="Customer") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_based_on_territory(self): + territory = frappe.db.get_value("Customer", CUSTOMER, "territory") + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Territory": territory}, cols, based_on="Territory") + self.deliver(customer=CUSTOMER) + after = self.values({"Territory": territory}, cols, based_on="Territory") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_group_by_item_under_customer(self): + # based_on=Customer with group_by=Item produces an item-wise breakdown row + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, based_on="Customer", group_by="Item") + self.deliver(customer=CUSTOMER) + after = self.values({"Item": ITEM}, cols, based_on="Customer", group_by="Item") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 5) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) diff --git a/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py new file mode 100644 index 00000000000..f930edad2df --- /dev/null +++ b/erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/test_fifo_queue_vs_qty_after_transaction_comparison.py @@ -0,0 +1,56 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.fifo_queue_vs_qty_after_transaction_comparison.fifo_queue_vs_qty_after_transaction_comparison import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestFifoQueueVsQtyAfterTransactionComparison(ERPNextTestSuite): + def run_report(self, filters: dict) -> list: + return execute(frappe._dict(filters))[1] + + def test_healthy_fifo_item_no_mismatch(self): + item = "_Test Item" + warehouse = "Stores - _TC" + frappe.db.set_value("Item", item, "valuation_method", "FIFO") + + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=5, rate=120, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=warehouse, qty=4, posting_date="2026-06-02") + + data = self.run_report({"company": "_Test Company", "item_code": item, "warehouse": warehouse}) + + item_codes = [row.get("item_code") for row in data if row] + self.assertNotIn(item, item_codes) + + def test_queue_out_of_sync_is_flagged(self): + item = "_Test Item 2" + warehouse = "Stores - _TC" + frappe.db.set_value("Item", item, "valuation_method", "FIFO") + + entry = make_stock_entry( + item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date="2026-06-01" + ) + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": entry.name, "item_code": item, "warehouse": warehouse}, + "name", + ) + + # corrupt the running balance so it no longer matches the FIFO queue (the queue holds 10, + # but the stored qty_after_transaction now claims 7) + frappe.db.set_value("Stock Ledger Entry", sle, "qty_after_transaction", 7, update_modified=False) + + data = self.run_report({"company": "_Test Company", "item_code": item, "warehouse": warehouse}) + + flagged = {row.get("name") for row in data if row} + self.assertIn(sle, flagged) + + def test_requires_a_filter(self): + with self.assertRaises(frappe.ValidationError): + self.run_report({"company": "_Test Company"}) diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py new file mode 100644 index 00000000000..cee67261b7d --- /dev/null +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/test_incorrect_balance_qty_after_transaction.py @@ -0,0 +1,65 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.incorrect_balance_qty_after_transaction.incorrect_balance_qty_after_transaction import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + +WAREHOUSE = "Stores - _TC" +COMPANY = "_Test Company" + + +class TestIncorrectBalanceQtyAfterTransaction(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE}) + filters.update(extra) + return execute(filters)[1] + + def test_healthy_stock_not_flagged(self): + item = "_Test Item" + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=4, rate=100, posting_date="2026-06-02") + + data = self.run_report(item_code=item) + flagged = [row for row in data if row.get("item_code") == item] + self.assertEqual(flagged, []) + + def test_inconsistent_balance_qty_is_flagged(self): + # a unique item keeps this SLE the only ledger entry for the item/warehouse + item = make_item(properties={"is_stock_item": 1}).name + entry = make_stock_entry( + item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01" + ) + + # Corrupt the running balance so it no longer matches the cumulative actual_qty -- + # exactly the inconsistency this report exists to detect. set_value bypasses the + # ledger recompute that would otherwise keep the two in sync. + sle_name = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": entry.name, "item_code": item, "warehouse": WAREHOUSE}, + "name", + ) + frappe.db.set_value("Stock Ledger Entry", sle_name, "qty_after_transaction", 5) + + flagged = [row for row in self.run_report(item_code=item) if row.get("name") == sle_name] + self.assertEqual(len(flagged), 1, "The tampered stock ledger entry should be flagged") + row = flagged[0] + self.assertEqual(row["expected_balance_qty"], 10) # cumulative actual_qty + self.assertEqual(row["qty_after_transaction"], 5) # tampered balance + self.assertEqual(row["differnce"], 5) + + def test_sequence_of_movements_not_flagged(self): + item = "_Test Item 2" + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=20, rate=50, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=5, rate=50, posting_date="2026-06-02") + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=8, rate=50, posting_date="2026-06-03") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=3, rate=50, posting_date="2026-06-04") + + data = self.run_report(item_code=item) + flagged = [row for row in data if row.get("item_code") == item] + self.assertEqual(flagged, []) diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py new file mode 100644 index 00000000000..86f32077bea --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/test_incorrect_serial_and_batch_bundle.py @@ -0,0 +1,83 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestIncorrectSerialAndBatchBundle(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company"}) + filters.update(extra) + return execute(filters)[1] + + def test_healthy_bundles_not_flagged(self): + batch_item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "ISBB-.#####", + } + ).name + serial_item = "_Test Serialized Item With Series" + + make_stock_entry( + item_code=batch_item, + qty=10, + rate=100, + to_warehouse="Stores - _TC", + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=serial_item, + qty=3, + rate=100, + to_warehouse="Stores - _TC", + posting_date="2026-06-01", + ) + + data = self.run_report() + + bundles = frappe.get_all( + "Serial and Batch Bundle", + filters={"item_code": ["in", [batch_item, serial_item]]}, + pluck="name", + ) + + flagged_names = {row.get("name") for row in data} + self.assertFalse( + flagged_names.intersection(bundles), + msg="Healthy serial/batch bundles should not be flagged as incorrect.", + ) + + def test_unlinked_bundle_is_flagged(self): + # an actual incorrect state: a submitted Serial and Batch Bundle left without any linking + # Stock Ledger Entry (e.g. the SLE was purged but the bundle survived) + batch_item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "ISBB-ORPHAN-.#####", + } + ).name + + entry = make_stock_entry( + item_code=batch_item, qty=5, rate=100, to_warehouse="Stores - _TC", posting_date="2026-06-01" + ) + bundle = frappe.db.get_value("Serial and Batch Bundle", {"voucher_no": entry.name}, "name") + self.assertTrue(bundle) + + # orphan the bundle: drop the Stock Ledger Entry that referenced it + frappe.db.delete("Stock Ledger Entry", {"serial_and_batch_bundle": bundle}) + + flagged = {row.get("name"): row for row in self.run_report()} + self.assertIn(bundle, flagged) + self.assertEqual(flagged[bundle]["is_cancelled"], 0) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py new file mode 100644 index 00000000000..81f736928fe --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py @@ -0,0 +1,81 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.incorrect_serial_no_valuation.incorrect_serial_no_valuation import execute +from erpnext.tests.utils import ERPNextTestSuite + +SERIAL_ITEM = "_Test Serialized Item With Series" +WAREHOUSE = "Stores - _TC" + + +class TestIncorrectSerialNoValuation(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company"}) + filters.update(extra) + return execute(filters)[1] + + def test_healthy_serial_item_not_flagged(self): + make_stock_entry( + item_code=SERIAL_ITEM, + to_warehouse=WAREHOUSE, + qty=3, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=SERIAL_ITEM, + from_warehouse=WAREHOUSE, + qty=1, + posting_date="2026-06-02", + ) + + data = self.run_report(item_code=SERIAL_ITEM) + + flagged_items = {row.get("item_code") for row in data if isinstance(row, dict)} + self.assertNotIn(SERIAL_ITEM, flagged_items) + + def test_only_balance_row_when_filtered_to_healthy_item(self): + make_stock_entry( + item_code=SERIAL_ITEM, + to_warehouse=WAREHOUSE, + qty=3, + rate=100, + posting_date="2026-06-01", + ) + + data = self.run_report(item_code=SERIAL_ITEM) + + # The report always appends a single "Balance" summary row. A healthy + # serial item contributes no detail rows, so only that summary remains. + self.assertEqual(len(data), 1) + self.assertEqual(data[-1].get("qty"), 0) + self.assertEqual(data[-1].get("valuation_rate"), 0) + + def test_mismatched_in_out_valuation_is_flagged(self): + # fresh serial item so only this test's serial movements are considered + item = make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "ISV-BAD-.#####"} + ).name + + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=1, rate=100, posting_date="2026-06-01") + serial_no = frappe.get_all("Serial No", filters={"item_code": item}, pluck="name")[0] + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=1, posting_date="2026-06-02") + + # corrupt the outgoing valuation so the serial's in (100) and out no longer cancel: + # net qty is 0 but a residual value remains, which the report must flag + frappe.db.set_value( + "Serial and Batch Entry", + {"serial_no": serial_no, "qty": ["<", 0]}, + "incoming_rate", + 60, + update_modified=False, + ) + + data = self.run_report(item_code=item) + + flagged_serials = {row.get("serial_no") for row in data if isinstance(row, dict)} + self.assertIn(serial_no, flagged_serials) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py new file mode 100644 index 00000000000..49e36c0429a --- /dev/null +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -0,0 +1,99 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.item_prices.item_prices import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestItemPrices(ERPNextTestSuite): + """Correctness tests for the Item Prices report.""" + + def run_report(self, **extra): + filters = frappe._dict({"items": "Enabled Items only", **extra}) + return execute(filters)[:2] + + # The report returns string-format columns ("Label:fieldtype:width"); resolve positions + # by label so the tests self-correct if the column order changes. + @staticmethod + def labels(columns): + return [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + + def row_for(self, columns, data, item_code): + item_idx = self.labels(columns).index("Item") + for row in data: + if row[item_idx] == item_code: + return row + self.fail(f"No report row found for item {item_code}") + return None + + def cell(self, columns, row, label): + return row[self.labels(columns).index(label)] + + def test_item_selling_price_listed(self): + """A Standard Selling Item Price shows up in the Sales Price List column.""" + item = "_Test Item" + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item, + "price_list": "Standard Selling", + "price_list_rate": 250, + } + ).insert() + + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertIn("250.0", self.cell(columns, row, "Sales Price List")) + self.assertIn("Standard Selling", self.cell(columns, row, "Sales Price List")) + # A selling price must not leak into the buying column. + self.assertNotIn("250.0", self.cell(columns, row, "Purchase Price List") or "") + self.assertNotIn("Standard Selling", self.cell(columns, row, "Purchase Price List") or "") + + def test_item_buying_price_listed(self): + """A Standard Buying Item Price shows up in the Purchase Price List column.""" + item = "_Test Item 2" + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item, + "price_list": "Standard Buying", + "price_list_rate": 175, + } + ).insert() + + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertIn("175.0", self.cell(columns, row, "Purchase Price List")) + self.assertIn("Standard Buying", self.cell(columns, row, "Purchase Price List")) + # A buying price must not leak into the selling column. + self.assertNotIn("175.0", self.cell(columns, row, "Sales Price List") or "") + self.assertNotIn("Standard Buying", self.cell(columns, row, "Sales Price List") or "") + + def test_last_purchase_rate_from_receipt(self): + """The latest purchase rate (from a Purchase Receipt) shows in the Last Purchase Rate column.""" + # a fresh item has no other committed purchase records, so it is the only (and latest) row + item = make_item(properties={"is_stock_item": 1, "is_purchase_item": 1}).name + make_purchase_receipt( + item_code=item, qty=5, rate=500, company="_Test Company", posting_date="2026-06-01" + ) + + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertEqual(self.cell(columns, row, "Last Purchase Rate"), 500) + + def test_valuation_rate_from_stock(self): + """The Bin valuation rate shows in the Valuation Rate column.""" + # a fresh item has no other committed bins, so its average valuation is exactly this receipt's rate + item = make_item(properties={"is_stock_item": 1}).name + make_stock_entry( + item_code=item, to_warehouse="Stores - _TC", qty=10, rate=250, posting_date="2026-06-01" + ) + + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertEqual(self.cell(columns, row, "Valuation Rate"), 250) diff --git a/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py new file mode 100644 index 00000000000..89d70d3ccdc --- /dev/null +++ b/erpnext/stock/report/item_wise_consumption/test_item_wise_consumption.py @@ -0,0 +1,50 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.item_wise_consumption.item_wise_consumption import execute +from erpnext.tests.utils import ERPNextTestSuite + +WH = "Stores - _TC" +# row: 0 item, 1 name, 2 desc, 3 uom, 4 consumed_qty, 5 consumed_amt, 6 delivered_qty, +# 7 delivered_amt, 8 total_qty, 9 total_amt, 10 suppliers + + +class TestItemWiseConsumption(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + {"company": "_Test Company", "from_date": "2026-01-01", "to_date": "2026-12-31", **extra} + ) + return execute(filters)[1] + + def test_consumed_vs_delivered_split(self): + # a uniquely-named item guarantees no residual stock/consumption from other + # tests leaks in -- the report aggregates an item across all warehouses. + item = make_item(properties={"is_stock_item": 1}).name + # purchase receipt gives the supplier mapping and stocks the item + make_purchase_receipt( + item_code=item, + qty=10, + rate=100, + warehouse=WH, + supplier="_Test Supplier", + posting_date="2026-06-01", + ) + # a material issue counts as "consumed", a delivery note counts as "delivered" + make_stock_entry(item_code=item, from_warehouse=WH, qty=4, posting_date="2026-06-02") + create_delivery_note(item_code=item, qty=3, warehouse=WH, posting_date="2026-06-03") + + row = next(r for r in self.run_report() if r[0] == item) + self.assertEqual(row[4], 4) # consumed qty + self.assertEqual(row[5], 400) # consumed amount + self.assertEqual(row[6], 3) # delivered qty + self.assertEqual(row[7], 300) # delivered amount + self.assertEqual(row[8], 7) # total qty = consumed + delivered + self.assertEqual(row[9], 700) # total amount = consumed + delivered amount + self.assertEqual(row[9], row[5] + row[7]) # total aggregates the two amounts + self.assertIn("_Test Supplier", row[10]) diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py new file mode 100644 index 00000000000..858ec9aa0ba --- /dev/null +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/test_itemwise_recommended_reorder_level.py @@ -0,0 +1,67 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt + +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.itemwise_recommended_reorder_level.itemwise_recommended_reorder_level import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + +WAREHOUSE = "Stores - _TC" + + +class TestItemwiseRecommendedReorderLevel(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"from_date": "2026-06-01", "to_date": "2026-06-10"}) + filters.update(extra) + return execute(filters)[1] + + def find_row(self, data, item_code): + for row in data: + if row[0] == item_code: + return row + return None + + def test_consumption_drives_recommendation(self): + item = "_Test Item" + frappe.db.set_value("Item", item, {"lead_time_days": 3, "safety_stock": 5}) + + # Receive stock, then issue a known total across dates inside the report window. + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=100, rate=10, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=20, posting_date="2026-06-03") + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=30, posting_date="2026-06-07") + + data = self.run_report(item_group="All Item Groups") + row = self.find_row(data, item) + self.assertIsNotNone(row, msg=f"Item {item} not found in report") + + float_precision = frappe.db.get_default("float_precision") + # Window 2026-06-01..2026-06-10 inclusive => diff = 10 days. + diff = 10 + total_outgoing = 50.0 # 20 + 30 issued + expected_avg = flt(total_outgoing / diff, float_precision) # 5.0 + expected_reorder = (expected_avg * 3) + 5 # avg * lead_time_days + safety_stock = 20 + + # Row shape: [item, item_name, item_group, brand, description, + # safety_stock, lead_time_days, consumed, delivered, total_outgoing, + # avg_daily_outgoing, reorder_level] + self.assertEqual(flt(row[7]), total_outgoing) # consumed + self.assertEqual(flt(row[8]), 0.0) # delivered + self.assertEqual(flt(row[9]), total_outgoing) # total outgoing + self.assertEqual(flt(row[10]), expected_avg) # avg daily outgoing + self.assertEqual(flt(row[11]), expected_reorder) # reorder level + + def test_no_consumption_yields_zero_outgoing(self): + item = "_Test Item 2" + frappe.db.set_value("Item", item, {"lead_time_days": 3, "safety_stock": 5}) + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=100, rate=10, posting_date="2026-06-01") + + row = self.find_row(self.run_report(), item) + self.assertIsNotNone(row) + self.assertEqual(flt(row[9]), 0.0) # total outgoing + self.assertEqual(flt(row[10]), 0.0) # avg daily outgoing + # With no consumption, reorder level falls back to safety_stock only. + self.assertEqual(flt(row[11]), 5.0) diff --git a/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py new file mode 100644 index 00000000000..e0b0385f710 --- /dev/null +++ b/erpnext/stock/report/negative_batch_report/test_negative_batch_report.py @@ -0,0 +1,64 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.negative_batch_report.negative_batch_report import execute +from erpnext.tests.utils import ERPNextTestSuite + +WAREHOUSE = "Stores - _TC" +COMPANY = "_Test Company" + + +class TestNegativeBatchReport(ERPNextTestSuite): + def run_report(self, item_code): + filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE, "item_code": item_code}) + return execute(filters)[1] + + def make_batch_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "NBR-.#####", + } + ).name + + def receive_batch(self, item, qty, posting_date): + """Receive `qty` of `item`, creating its batch, and return the batch no.""" + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=qty, rate=100, posting_date=posting_date) + return frappe.get_all("Batch", filters={"item": item}, pluck="name")[0] + + def test_healthy_batch_not_negative(self): + item = self.make_batch_item() + batch = self.receive_batch(item, 10, "2026-06-01") + # issue from the same batch, staying within its balance + make_stock_entry( + item_code=item, from_warehouse=WAREHOUSE, qty=4, batch_no=batch, posting_date="2026-06-02" + ) + + # received 10 then issued 4 -> running batch balance never goes negative + data = self.run_report(item) + self.assertFalse([row for row in data if row.get("batch_no") == batch]) + + def test_negative_batch_is_flagged(self): + # ERPNext blocks a negative batch balance at submission time (across several + # layers), so a genuinely negative batch only exists as corrupt historical + # data -- which is exactly what this report is meant to surface. Reproduce + # that state directly by forcing the batch's ledger quantity below zero. + item = self.make_batch_item() + batch = self.receive_batch(item, 10, "2026-06-10") + + sle = frappe.get_all("Stock Ledger Entry", filters={"item_code": item}, pluck="name")[0] + entry = frappe.get_all("Serial and Batch Entry", filters={"batch_no": batch}, pluck="name")[0] + frappe.db.set_value("Serial and Batch Entry", entry, "qty", -3) + frappe.db.set_value("Stock Ledger Entry", sle, {"actual_qty": -3, "qty_after_transaction": -3}) + + data = self.run_report(item) + flagged = [row for row in data if row.get("batch_no") == batch] + self.assertEqual(len(flagged), 1, "A batch with a negative running balance must be flagged") + self.assertEqual(flagged[0]["qty_after_transaction"], -3) + self.assertEqual(flagged[0]["warehouse"], WAREHOUSE) diff --git a/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py new file mode 100644 index 00000000000..4403db6fcd7 --- /dev/null +++ b/erpnext/stock/report/product_bundle_balance/test_product_bundle_balance.py @@ -0,0 +1,51 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.product_bundle_balance.product_bundle_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + +WH = "Stores - _TC" + + +class TestProductBundleBalance(ERPNextTestSuite): + def make_bundle(self, parent, child_qty): + bundle = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": parent}) + for item_code, qty in child_qty.items(): + bundle.append("items", {"item_code": item_code, "qty": qty}) + bundle.insert() + bundle.submit() + + def run_report(self, item_code): + filters = frappe._dict({"company": "_Test Company", "item_code": item_code}) + return execute(filters)[1] + + def test_bundle_qty_is_limited_by_scarcest_child(self): + # Reuse the bootstrap stock items as children. They start at zero in `Stores - _TC`, + # so transacting there gives a clean, deterministic balance for this warehouse's row. + parent = make_item(properties={"is_stock_item": 0, "is_sales_item": 1}).name + child_a = "_Test Item" + child_b = "_Test Item 2" + self.make_bundle(parent, {child_a: 2, child_b: 1}) + + make_stock_entry(item_code=child_a, to_warehouse=WH, qty=10, rate=100) + make_stock_entry(item_code=child_b, to_warehouse=WH, qty=3, rate=100) + + data = self.run_report(parent) + parent_row = next( + r for r in data if r["item_code"] == parent and r["indent"] == 0 and r["warehouse"] == WH + ) + # min(10 // 2, 3 // 1) = min(5, 3) = 3 buildable bundles + self.assertEqual(parent_row["bundle_qty"], 3) + + row_a = next( + r for r in data if r["item_code"] == child_a and r["indent"] == 1 and r["warehouse"] == WH + ) + row_b = next( + r for r in data if r["item_code"] == child_b and r["indent"] == 1 and r["warehouse"] == WH + ) + self.assertEqual((row_a["actual_qty"], row_a["minimum_qty"], row_a["bundle_qty"]), (10, 2, 5)) + self.assertEqual((row_b["actual_qty"], row_b["minimum_qty"], row_b["bundle_qty"]), (3, 1, 3)) diff --git a/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py new file mode 100644 index 00000000000..220f80616ed --- /dev/null +++ b/erpnext/stock/report/purchase_receipt_trends/test_purchase_receipt_trends.py @@ -0,0 +1,132 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.report.purchase_receipt_trends.purchase_receipt_trends import execute +from erpnext.tests.utils import ERPNextTestSuite + +ITEM = "_Test Item" +ITEM_GROUP = "_Test Item Group" +SUPPLIER = "_Test Supplier" + + +class TestPurchaseReceiptTrends(ERPNextTestSuite): + def run_report(self, **extra): + return self.run_report_full(**extra)[1] + + def run_report_full(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "fiscal_year": "_Test Fiscal Year 2026", + "period": "Yearly", + "based_on": "Item", + "group_by": "", + } + ) + filters.update(extra) + columns, data = execute(filters)[:2] + return columns, data + + # trend columns are "Label:fieldtype:width" strings; assert by label so the index + # stays correct across period / based_on / group_by combinations. + @staticmethod + def labels(columns): + return [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + + def find_row(self, columns, data, match): + labels = self.labels(columns) + for row in data: + if all(row[labels.index(label)] == value for label, value in match.items()): + return row + return None + + def value(self, columns, row, label): + if not row: + return 0 + return row[self.labels(columns).index(label)] or 0 + + def values(self, match, wanted_labels, **extra): + columns, data = self.run_report_full(**extra) + row = self.find_row(columns, data, match) + return {label: self.value(columns, row, label) for label in wanted_labels} + + def test_receipt_qty_in_trend(self): + # The report sums ALL purchase receipts for the item in the fiscal year, so capture + # any pre-existing baseline and assert only this receipt's contribution. + cols = ["_Test Fiscal Year 2026 (Qty)", "_Test Fiscal Year 2026 (Amt)"] + before = self.values({"Item": ITEM}, cols) + make_purchase_receipt( + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + after = self.values({"Item": ITEM}, cols) + self.assertEqual(after[cols[0]] - before[cols[0]], 10) + self.assertEqual(after[cols[1]] - before[cols[1]], 1000) + + def test_monthly_period_buckets(self): + cols = ["Jun (Qty)", "Jun (Amt)", "Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, period="Monthly") + make_purchase_receipt( + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + after = self.values({"Item": ITEM}, cols, period="Monthly") + # the June receipt lands only in the June bucket, and rolls up into the Total columns + self.assertEqual(after["Jun (Qty)"] - before["Jun (Qty)"], 10) + self.assertEqual(after["Jun (Amt)"] - before["Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_quarterly_period_buckets(self): + # 2026-06-01 falls in the Apr-Jun quarter + cols = ["Apr-Jun (Qty)", "Apr-Jun (Amt)", "Total(Qty)"] + before = self.values({"Item": ITEM}, cols, period="Quarterly") + make_purchase_receipt( + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + after = self.values({"Item": ITEM}, cols, period="Quarterly") + self.assertEqual(after["Apr-Jun (Qty)"] - before["Apr-Jun (Qty)"], 10) + self.assertEqual(after["Apr-Jun (Amt)"] - before["Apr-Jun (Amt)"], 1000) + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + + def test_based_on_supplier(self): + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Supplier": SUPPLIER}, cols, based_on="Supplier") + make_purchase_receipt( + item_code=ITEM, + qty=10, + rate=100, + supplier=SUPPLIER, + company="_Test Company", + posting_date="2026-06-01", + ) + after = self.values({"Supplier": SUPPLIER}, cols, based_on="Supplier") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_based_on_item_group(self): + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Item Group": ITEM_GROUP}, cols, based_on="Item Group") + make_purchase_receipt( + item_code=ITEM, qty=10, rate=100, company="_Test Company", posting_date="2026-06-01" + ) + after = self.values({"Item Group": ITEM_GROUP}, cols, based_on="Item Group") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) + + def test_group_by_item_under_supplier(self): + # based_on=Supplier with group_by=Item produces an item-wise breakdown row + cols = ["Total(Qty)", "Total(Amt)"] + before = self.values({"Item": ITEM}, cols, based_on="Supplier", group_by="Item") + make_purchase_receipt( + item_code=ITEM, + qty=10, + rate=100, + supplier=SUPPLIER, + company="_Test Company", + posting_date="2026-06-01", + ) + after = self.values({"Item": ITEM}, cols, based_on="Supplier", group_by="Item") + self.assertEqual(after["Total(Qty)"] - before["Total(Qty)"], 10) + self.assertEqual(after["Total(Amt)"] - before["Total(Amt)"], 1000) diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py b/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py new file mode 100644 index 00000000000..09e68e7ab0a --- /dev/null +++ b/erpnext/stock/report/serial_no_and_batch_traceability/test_serial_no_and_batch_traceability.py @@ -0,0 +1,98 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.serial_no_and_batch_traceability.serial_no_and_batch_traceability import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + +SERIAL_ITEM = "_Test Serialized Item With Series" + + +class TestSerialNoAndBatchTraceability(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company"}) + filters.update(extra) + return execute(filters)[1] + + def get_received_serial_no(self, receipt): + bundle = frappe.db.get_value( + "Stock Entry Detail", + {"parent": receipt.name, "item_code": SERIAL_ITEM}, + "serial_and_batch_bundle", + ) + return frappe.db.get_value("Serial and Batch Entry", {"parent": bundle}, "serial_no") + + def test_serial_movements_traced(self): + """Backward trace should surface the receipt voucher the serial came in through.""" + receipt = make_stock_entry( + item_code=SERIAL_ITEM, + to_warehouse="Stores - _TC", + qty=2, + rate=100, + posting_date="2026-06-01", + company="_Test Company", + ) + serial_no = self.get_received_serial_no(receipt) + + rows = self.run_report( + item_code=SERIAL_ITEM, + serial_nos=[serial_no], + traceability_direction="Backward", + ) + + traced = {row["reference_name"]: row for row in rows if row.get("reference_name")} + self.assertIn(receipt.name, traced) + + receipt_row = traced[receipt.name] + self.assertEqual(receipt_row["serial_no"], serial_no) + self.assertEqual(receipt_row["item_code"], SERIAL_ITEM) + self.assertEqual(receipt_row["reference_doctype"], "Stock Entry") + self.assertEqual(receipt_row["warehouse"], "Stores - _TC") + self.assertEqual(receipt_row["direction"], "Backward") + self.assertGreater(receipt_row["qty"], 0) + + def test_forward_and_backward_directions(self): + """'Both' should trace backward to the receipt and forward to the outward delivery.""" + receipt = make_stock_entry( + item_code=SERIAL_ITEM, + to_warehouse="Stores - _TC", + qty=2, + rate=100, + posting_date="2026-06-01", + company="_Test Company", + ) + serial_no = self.get_received_serial_no(receipt) + + delivery_note = create_delivery_note( + item_code=SERIAL_ITEM, + qty=1, + serial_no=[serial_no], + warehouse="Stores - _TC", + customer="_Test Customer", + posting_date="2026-06-03", + company="_Test Company", + ) + + rows = self.run_report( + item_code=SERIAL_ITEM, + serial_nos=[serial_no], + traceability_direction="Both", + ) + + traced = {row["reference_name"]: row for row in rows if row.get("reference_name")} + + self.assertIn(receipt.name, traced) + self.assertEqual(traced[receipt.name]["direction"], "Backward") + + self.assertIn(delivery_note.name, traced) + forward_row = traced[delivery_note.name] + self.assertEqual(forward_row["reference_doctype"], "Delivery Note") + self.assertEqual(forward_row["serial_no"], serial_no) + self.assertEqual(forward_row["direction"], "Forward") + self.assertEqual(forward_row["customer"], delivery_note.customer) + self.assertLess(forward_row["qty"], 0) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index a7b8a8a622a..c52d466b897 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -505,7 +505,7 @@ class FIFOSlots: self._add_serial_fifo_slots(row, fifo_queue, serial_nos) elif batch_nos and row.get("has_batch_no"): self._add_batch_fifo_slots(row, fifo_queue, batch_nos) - elif fifo_queue and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0: + elif fifo_queue and is_qty_slot(fifo_queue[0]) and flt(fifo_queue[0][FIFO_QTY_INDEX]) <= 0: self._add_to_negative_fifo_head(row, fifo_queue) else: fifo_queue.append([flt(row.actual_qty), row.posting_date, flt(row.stock_value_difference)]) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 37b4b081cfe..7809451744d 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1434,6 +1434,47 @@ class TestStockAgeing(ERPNextTestSuite): self.assertEqual(item_result["total_qty"], -4.0) self.assertEqual(item_result["fifo_queue"], [[batch_no, 1, -4.0, "2021-11-10", -40.0]]) + def test_untagged_receipt_with_negative_batch_head(self): + """An incoming SLE without batch details must not treat a negative + batch slot at the queue head as a qty slot (TypeError: str += float).""" + sle = [ + frappe._dict( + name="Enclosure Item", + actual_qty=-10, + qty_after_transaction=-10, + stock_value_difference=-100, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no="QI-06448", + ), + frappe._dict( + name="Enclosure Item", + actual_qty=45, + qty_after_transaction=35, + stock_value_difference=1051.65, + warehouse="WH 1", + posting_date="2021-12-05", + voucher_type="Purchase Receipt", + voucher_no="002", + has_serial_no=False, + serial_no=None, + batch_no=None, + serial_and_batch_bundle="SABB-00001294", + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + queue = slots["Enclosure Item"]["fifo_queue"] + + self.assertEqual(slots["Enclosure Item"]["total_qty"], 35.0) + self.assertEqual(queue[0], ["QI-06448", None, -10.0, "2021-12-01", -100.0]) + self.assertEqual(queue[1], [45.0, "2021-12-05", 1051.65]) + def test_batchwise_valuation_stock_reconciliation_with_bundle(self): from frappe.utils import add_days, getdate, nowdate diff --git a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py index 0795bc6ad79..7eabe37cc91 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py @@ -6,30 +6,87 @@ from frappe.utils import today from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import ( create_reposting_entries, execute, ) from erpnext.tests.utils import ERPNextTestSuite -PI_COMPANY = "_Test Company with perpetual inventory" +COMPANY = "_Test Company with perpetual inventory" PI_STORES = "Stores - TCP1" class TestStockAndAccountValueComparison(ERPNextTestSuite): + def test_balanced_warehouse_not_flagged(self): + warehouse = create_warehouse("_Test SAVC WH", company=COMPANY) + account = frappe.get_value("Warehouse", warehouse, "account") + item = "_Test Item" + + make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=10, + rate=100, + company=COMPANY, + posting_date="2026-06-01", + ) + + # Filtering by the isolated account restricts both the stock-ledger and GL + # scans to this fresh warehouse's account only. + rows = self.run_report(account=account) + + # The report lists only mismatches (rows where abs(difference_value) > 0.1), + # keyed per voucher. A balanced perpetual warehouse posts equal stock-ledger + # and GL values for the receipt voucher, so nothing should be flagged. + self.assertEqual(rows, []) + + def test_stock_account_gl_mismatch_is_flagged(self): + warehouse = create_warehouse("_Test SAVC Mismatch WH", company=COMPANY) + account = frappe.get_value("Warehouse", warehouse, "account") + + receipt = make_stock_entry( + item_code="_Test Item", + to_warehouse=warehouse, + qty=10, + rate=100, + company=COMPANY, + posting_date="2026-06-01", + ) + + # Simulate corruption: the stock-account GL entry for this receipt drifts out of sync + # with the stock ledger (stock value stays 1000, but the account only shows 600). + frappe.db.set_value( + "GL Entry", + {"voucher_no": receipt.name, "account": account, "is_cancelled": 0}, + "debit_in_account_currency", + 600, + update_modified=False, + ) + + rows = self.run_report(account=account) + + row = next((r for r in rows if r["voucher_no"] == receipt.name), None) + self.assertIsNotNone(row, "Tampered GL entry should cause the voucher to appear in the report") + self.assertEqual(row["ledger_type"], "Stock Ledger Entry") + self.assertEqual(row["stock_value"], 1000) # unchanged stock ledger value + self.assertEqual(row["account_value"], 600) # tampered GL value + self.assertEqual(row["difference_value"], 400) # 1000 - 600, above the 0.1 threshold + def test_purchase_voucher_reposted_transaction_based(self): # A Purchase Receipt whose GL entries are missing must surface in the report and, when reposted # from it, be reposted Transaction-based (so its own GL is regenerated) rather than the slower # Item-and-Warehouse based reposting. item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name - pr = make_purchase_receipt(item_code=item, company=PI_COMPANY, warehouse=PI_STORES, qty=5, rate=100) + pr = make_purchase_receipt(item_code=item, company=COMPANY, warehouse=PI_STORES, qty=5, rate=100) # Simulate the out-of-sync state: stock ledger exists but the accounting ledger does not. frappe.db.delete("GL Entry", {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}) # The receipt now shows up in the comparison report (stock value 500 vs account value 0). - filters = frappe._dict(company=PI_COMPANY, as_on_date=today()) + filters = frappe._dict(company=COMPANY, as_on_date=today()) _columns, data = execute(filters) row = next((d for d in data if d.get("voucher_no") == pr.name), None) @@ -37,7 +94,7 @@ class TestStockAndAccountValueComparison(ERPNextTestSuite): self.assertEqual(row.get("voucher_type"), "Purchase Receipt") # Repost from the report. - create_reposting_entries([row], PI_COMPANY) + create_reposting_entries([row], COMPANY) # A Transaction-based Repost Item Valuation must have been created for this voucher... transaction_rivs = frappe.get_all( @@ -55,3 +112,8 @@ class TestStockAndAccountValueComparison(ERPNextTestSuite): filters={"based_on": "Item and Warehouse", "item_code": item}, ) self.assertFalse(item_wh_rivs, "Purchase vouchers must not be reposted Item-and-Warehouse based") + + def run_report(self, **extra): + filters = {"company": COMPANY, "as_on_date": "2026-12-31"} + filters.update(extra) + return execute(frappe._dict(filters))[1] diff --git a/erpnext/stock/report/stock_ledger/test_stock_ledger.py b/erpnext/stock/report/stock_ledger/test_stock_ledger.py new file mode 100644 index 00000000000..0b384630d10 --- /dev/null +++ b/erpnext/stock/report/stock_ledger/test_stock_ledger.py @@ -0,0 +1,182 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.stock_ledger.stock_ledger import execute +from erpnext.tests.utils import ERPNextTestSuite + +WH = "Stores - _TC" +WH2 = "Finished Goods - _TC" +ITEM = "_Test Item" +ITEM2 = "_Test Item 2" +SERIAL_ITEM = "_Test Serialized Item With Series" + + +class TestStockLedgerReport(ERPNextTestSuite): + def make_batch_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SL-BCH-.#####", + } + ).name + + def run_report(self, item_code, warehouse=None, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + "valuation_field_type": "Currency", + "item_code": [item_code] if isinstance(item_code, str) else item_code, + **extra, + } + ) + if warehouse: + filters["warehouse"] = [warehouse] if isinstance(warehouse, str) else warehouse + return execute(filters)[1] + + def sle_rows(self, item_code, warehouse=WH, **extra): + # scope to the clean warehouse so the committed baseline stock of reused master + # items (in `_Test Warehouse - _TC`) does not leak in; drop the synthetic + # "'Opening'" row and keep only this item's ledger lines + rows = self.run_report(item_code, warehouse=warehouse, **extra) + return [row for row in rows if row.get("item_code") == item_code] + + def test_receipt_shows_in_qty_and_balance(self): + item = ITEM + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + + (row,) = self.sle_rows(item) + self.assertEqual(row["in_qty"], 10) + self.assertEqual(row["out_qty"], 0) + self.assertEqual(row["qty_after_transaction"], 10) + self.assertEqual(row["incoming_rate"], 100) + self.assertEqual(row["valuation_rate"], 100) + self.assertEqual(row["stock_value"], 1000) + self.assertEqual(row["stock_value_difference"], 1000) + + def test_issue_shows_out_qty_and_outgoing_rate(self): + item = ITEM + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WH, qty=4, posting_date="2026-06-02") + + issue = self.sle_rows(item)[-1] + self.assertEqual(issue["in_qty"], 0) + self.assertEqual(issue["out_qty"], -4) + self.assertEqual(issue["qty_after_transaction"], 6) + self.assertEqual(issue["in_out_rate"], 100) # stock_value_difference / actual_qty + self.assertEqual(issue["stock_value"], 600) + + def test_running_balance_across_transactions(self): + item = ITEM + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH, qty=5, rate=100, posting_date="2026-06-02") + make_stock_entry(item_code=item, from_warehouse=WH, qty=3, posting_date="2026-06-03") + + balances = [row["qty_after_transaction"] for row in self.sle_rows(item)] + self.assertEqual(balances, [10, 15, 12]) + + def test_moving_average_valuation(self): + item = ITEM + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=200, posting_date="2026-06-02") + + latest = self.sle_rows(item)[-1] + # (10*100 + 10*200) / 20 = 150 + self.assertEqual(latest["valuation_rate"], 150) + self.assertEqual(latest["stock_value"], 3000) + + def test_item_code_filter_excludes_other_items(self): + item_a = ITEM + item_b = ITEM2 + make_stock_entry(item_code=item_a, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item_b, to_warehouse=WH, qty=7, rate=100, posting_date="2026-06-01") + + item_codes = {row["item_code"] for row in self.run_report(item_a)} + self.assertEqual(item_codes, {item_a}) + + def test_warehouse_filter(self): + item = ITEM + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH2, qty=5, rate=100, posting_date="2026-06-01") + + warehouses = {row["warehouse"] for row in self.sle_rows(item, warehouse=WH2)} + self.assertEqual(warehouses, {WH2}) + + def test_voucher_no_filter(self): + item = ITEM + se = make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, to_warehouse=WH, qty=5, rate=100, posting_date="2026-06-02") + + rows = self.sle_rows(item, voucher_no=se.name) + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0]["voucher_no"], se.name) + + def test_date_range_excludes_out_of_range_entries(self): + item = ITEM + se = make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2025-12-01") + + # 2026 window must not include the 2025 entry + self.assertEqual(self.sle_rows(item), []) + # widening the window back to 2025 brings it in + in_window = self.run_report(item, from_date="2025-01-01", to_date="2025-12-31") + self.assertIn(se.name, {row.get("voucher_no") for row in in_window}) + + def test_opening_balance_row(self): + item = ITEM + # stock received before the reporting window should surface as the opening balance + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2025-12-01") + + data = self.run_report(item, warehouse=WH) + opening = data[0] + self.assertEqual(opening["item_code"], "'Opening'") + self.assertEqual(opening["qty_after_transaction"], 10) + self.assertEqual(opening["stock_value"], 1000) + + def test_bundle_not_segregated_by_default(self): + item = SERIAL_ITEM + # a single receipt of 3 serials is one ledger line when the filter is off + make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") + + (row,) = self.sle_rows(item) + self.assertEqual(row["in_qty"], 3) + self.assertEqual(row["qty_after_transaction"], 3) + + def test_serial_bundle_segregated_into_per_serial_rows(self): + item = SERIAL_ITEM + make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") + + rows = self.sle_rows(item, segregate_serial_batch_bundle=1) + # the one receipt is split into one row per serial number + self.assertEqual(len(rows), 3) + self.assertTrue(all(row["in_qty"] == 1 for row in rows)) + self.assertEqual(len({row["serial_no"] for row in rows}), 3) + # running balance accumulates across the segregated rows + self.assertEqual([row["qty_after_transaction"] for row in rows], [1, 2, 3]) + + def test_segregated_issue_rows_show_out_qty_per_serial(self): + item = SERIAL_ITEM + make_stock_entry(item_code=item, to_warehouse=WH, qty=3, rate=100, posting_date="2026-06-01") + make_stock_entry(item_code=item, from_warehouse=WH, qty=2, posting_date="2026-06-02") + + rows = self.sle_rows(item, segregate_serial_batch_bundle=1) + issue_rows = [row for row in rows if row["out_qty"]] + self.assertEqual(len(issue_rows), 2) + self.assertTrue(all(row["out_qty"] == -1 for row in issue_rows)) + self.assertTrue(all(row["in_out_rate"] == 100 for row in issue_rows)) + + def test_batch_bundle_segregated_shows_batch_no(self): + item = self.make_batch_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date="2026-06-01") + + (row,) = self.sle_rows(item, segregate_serial_batch_bundle=1) + self.assertTrue(row["batch_no"]) + self.assertEqual(row["in_qty"], 10) + self.assertEqual(row["qty_after_transaction"], 10) diff --git a/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py new file mode 100644 index 00000000000..1787a0a75e5 --- /dev/null +++ b/erpnext/stock/report/stock_ledger_variance/test_stock_ledger_variance.py @@ -0,0 +1,106 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.tests.utils import ERPNextTestSuite + + +class TestStockLedgerVariance(ERPNextTestSuite): + def run_report(self, **extra): + from erpnext.stock.report.stock_ledger_variance.stock_ledger_variance import execute + + filters = {"company": "_Test Company"} + filters.update(extra) + + return execute(frappe._dict(filters))[1] + + def test_healthy_stock_has_no_variance(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = "_Test Item" + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") + + make_stock_entry( + item_code=item, + to_warehouse="Stores - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item, + from_warehouse="Stores - _TC", + qty=4, + posting_date="2026-06-02", + ) + + # A clean receipt followed by a clean issue keeps the ledger consistent, + # so the corruption detector must not flag any entry for this item. + data = self.run_report(item_code=item) + self.assertFalse([row for row in data if row.get("item_code") == item]) + + qty_data = self.run_report(item_code=item, difference_in="Qty") + self.assertFalse([row for row in qty_data if row.get("item_code") == item]) + + def test_multiple_clean_movements_no_variance(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = "_Test Item" + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") + + make_stock_entry( + item_code=item, + to_warehouse="Stores - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item, + to_warehouse="Stores - _TC", + qty=5, + rate=120, + posting_date="2026-06-02", + ) + make_stock_entry( + item_code=item, + to_warehouse="Stores - _TC", + qty=8, + rate=90, + posting_date="2026-06-03", + ) + make_stock_entry( + item_code=item, + from_warehouse="Stores - _TC", + qty=6, + posting_date="2026-06-04", + ) + + # Several receipts at different rates plus an issue still produce a + # self-consistent ledger, so no variance rows are expected. + data = self.run_report(item_code=item) + self.assertFalse([row for row in data if row.get("item_code") == item]) + + def test_incorrect_balance_qty_is_flagged(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item = "_Test Item 2" + warehouse = "Stores - _TC" + frappe.db.set_value("Item", item, "valuation_method", "Moving Average") + + entry = make_stock_entry( + item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date="2026-06-01" + ) + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": entry.name, "item_code": item, "warehouse": warehouse}, + "name", + ) + + # corrupt the stored running balance (expected 10 from the receipt, but now claims 7) + frappe.db.set_value("Stock Ledger Entry", sle, "qty_after_transaction", 7, update_modified=False) + + data = self.run_report(item_code=item, difference_in="Qty") + row = next(r for r in data if r.get("item_code") == item) + self.assertEqual(row["difference_in_qty"], -3) # 7 (stored) - 10 (expected) diff --git a/erpnext/stock/report/stock_qty_vs_batch_qty/test_stock_qty_vs_batch_qty.py b/erpnext/stock/report/stock_qty_vs_batch_qty/test_stock_qty_vs_batch_qty.py new file mode 100644 index 00000000000..e68365c491f --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_batch_qty/test_stock_qty_vs_batch_qty.py @@ -0,0 +1,67 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.stock_qty_vs_batch_qty.stock_qty_vs_batch_qty import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestStockQtyVsBatchQty(ERPNextTestSuite): + def run_report(self, **extra): + return execute(frappe._dict({"company": "_Test Company", **extra}))[1] + + def make_batch_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SQB-.#####", + } + ).name + + def rows_for_item(self, data, item): + return [row for row in data if row["item_code"] == item] + + def test_stock_qty_matches_batch_qty(self): + item = self.make_batch_item() + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + + # The report only lists batches where stock qty and batch qty differ. + # A healthy item has difference == 0, so it must be absent from results. + data = self.run_report(item=item) + self.assertEqual(self.rows_for_item(data, item), []) + + def test_mismatch_reports_difference(self): + item = self.make_batch_item() + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + posting_date="2026-06-01", + ) + batch_no = frappe.db.get_value("Batch", {"item": item}, "name") + self.assertTrue(batch_no) + + # Corrupt the stored batch qty so it no longer matches actual stock qty (10). + frappe.db.set_value("Batch", batch_no, "batch_qty", 7) + + data = self.run_report(item=item, batch=batch_no) + rows = self.rows_for_item(data, item) + self.assertEqual(len(rows), 1) + + row = rows[0] + self.assertEqual(row["batch"], batch_no) + self.assertEqual(row["batch_qty"], 7) + self.assertEqual(row["stock_qty"], 10) + self.assertEqual(row["difference"], 3) diff --git a/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py b/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py new file mode 100644 index 00000000000..62f32465894 --- /dev/null +++ b/erpnext/stock/report/total_stock_summary/test_total_stock_summary.py @@ -0,0 +1,32 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.total_stock_summary.total_stock_summary import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestTotalStockSummary(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company", "group_by": "Warehouse", **extra}) + return execute(filters)[1] + + def test_warehouse_wise_quantity(self): + item = "_Test Item" + warehouse = "Stores - _TC" # clean zero baseline for _Test Item + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100) + + # rows are (warehouse, item_code, description, actual_qty) + row = next(r for r in self.run_report() if r[0] == warehouse and r[1] == item) + self.assertEqual(row[3], 10) + + def test_only_non_zero_bins_are_listed(self): + item = "_Test Item 2" + warehouse = "Stores - _TC" # clean zero baseline for _Test Item 2 + # receive then issue everything -> bin actual_qty back to zero + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=5, rate=100) + make_stock_entry(item_code=item, from_warehouse=warehouse, qty=5) + + self.assertFalse([r for r in self.run_report() if r[0] == warehouse and r[1] == item]) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py new file mode 100644 index 00000000000..a94cd21f02a --- /dev/null +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/test_warehouse_wise_item_balance_age_and_value.py @@ -0,0 +1,55 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.warehouse_wise_item_balance_age_and_value.warehouse_wise_item_balance_age_and_value import ( + execute, +) +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWarehouseWiseItemBalanceAgeAndValue(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict( + { + "company": "_Test Company", + "from_date": "2026-01-01", + "to_date": "2026-12-31", + "warehouse": "Stores - _TC", + } + ) + filters.update(extra) + return execute(filters)[1] + + def test_balance_qty_and_value(self): + item_code = "_Test Item" + warehouse = "Stores - _TC" + + make_stock_entry( + item_code=item_code, + to_warehouse=warehouse, + qty=10, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item_code, + from_warehouse=warehouse, + qty=4, + posting_date="2026-06-02", + ) + + data = self.run_report(item_code=item_code) + + # With a single (leaf) warehouse filter the row shape is: + # [item, item_name, item_group, brand, value, age, bal_qty] + rows = [row for row in data if row[0] == item_code] + self.assertEqual(len(rows), 1) + + row = rows[0] + # index 6 -> balance qty in the filtered warehouse + self.assertEqual(row[6], 6) + # index 4 -> total stock value (6 units @ 100) + self.assertEqual(row[4], 600) diff --git a/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py b/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py new file mode 100644 index 00000000000..16bcc181d87 --- /dev/null +++ b/erpnext/stock/report/warehouse_wise_stock_balance/test_warehouse_wise_stock_balance.py @@ -0,0 +1,30 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse +from erpnext.stock.report.warehouse_wise_stock_balance.warehouse_wise_stock_balance import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestWarehouseWiseStockBalance(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company", **extra}) + return execute(filters)[1] + + def row(self, data, warehouse): + return next(w for w in data if w["name"] == warehouse) + + def test_balance_and_parent_accumulation(self): + parent = create_warehouse("_Test WWSB Parent", properties={"is_group": 1}) + child = create_warehouse("_Test WWSB Child", properties={"parent_warehouse": parent}) + + make_stock_entry(item_code="_Test Item", to_warehouse=child, qty=10, rate=100) + + data = self.run_report() + # stock balance = sum of stock value difference (10 * 100) + self.assertEqual(self.row(data, child)["stock_balance"], 1000) + # the group warehouse rolls up its children + self.assertEqual(self.row(data, parent)["stock_balance"], 1000) diff --git a/erpnext/stock/services/serial_batch_bundle_service.py b/erpnext/stock/services/serial_batch_bundle_service.py index 29d732c1e32..2e752371ed5 100644 --- a/erpnext/stock/services/serial_batch_bundle_service.py +++ b/erpnext/stock/services/serial_batch_bundle_service.py @@ -176,6 +176,10 @@ class SerialBatchBundleService: parent_details = self.get_parent_details_for_packed_items() for row in self.doc.get(table_name): + item_code = row.get("rm_item_code") or row.get("item_code") + if not item_code or not self.is_serial_batch_item(item_code): + continue + if ( not via_landed_cost_voucher and row.serial_and_batch_bundle diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f03e75c2b51..229837d5eed 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -5,6 +5,7 @@ import copy import gzip import json from collections import deque +from contextlib import nullcontext import frappe from frappe import _, bold, scrub @@ -261,6 +262,29 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): return sle +# A repost waits this long for another repost's per-(item, warehouse) gate before giving up. Kept +# well under the 1800s repost job timeout so a wait can't burn the whole budget, and short enough +# that a contended worker re-queues (recoverable QueryTimeoutError) and frees the slot for other +# items instead of pinning it. +REPOST_LOCK_TIMEOUT = 300 + + +def repost_gate(item_code, warehouse): + """Serialize concurrent background reposts of the same (item, warehouse) with a session-level + advisory lock taken before the inner `... for update` row locks, so they take turns instead of + racing into a lock-order deadlock. Row locks still enforce correctness; this only cuts the + deadlock/retry churn. Scope is repost-vs-repost only -- the synchronous repost_current_voucher + submit path is deliberately not gated (blocking a submit behind a background repost would be a + worse regression) and keeps relying on the existing deadlock retry. Postgres only: MariaDB + keeps the plain deadlock-retry path.""" + # hasattr keeps this a graceful opt-in: on an ERPNext predating frappe.db.advisory_lock, fall + # back to no gate rather than raising and marking the Repost Item Valuation permanently Failed. + if frappe.db.db_type == "postgres" and hasattr(frappe.db, "advisory_lock"): + # Tuple key: a colon in item_code/warehouse can't collide two distinct pairs onto one lock. + return frappe.db.advisory_lock(("stock_repost", item_code, warehouse), timeout=REPOST_LOCK_TIMEOUT) + return nullcontext() + + def repost_future_sle( items_to_be_repost=None, voucher_type=None, @@ -289,22 +313,25 @@ def repost_future_sle( while index < len(items_to_be_repost): validate_item_warehouse(items_to_be_repost[index]) - obj = update_entries_after( - { - "item_code": items_to_be_repost[index].get("item_code"), - "warehouse": items_to_be_repost[index].get("warehouse"), - "posting_date": items_to_be_repost[index].get("posting_date"), - "posting_time": items_to_be_repost[index].get("posting_time"), - "creation": items_to_be_repost[index].get("creation"), - "current_idx": index, - "items_to_be_repost": items_to_be_repost, - "repost_doc": doc, - "repost_affected_transaction": repost_affected_transaction, - "item_wh_wise_last_posted_sle": resume_item_wh_wise_last_posted_sle, - }, - allow_negative_stock=allow_negative_stock, - via_landed_cost_voucher=via_landed_cost_voucher, - ) + item_code = items_to_be_repost[index].get("item_code") + warehouse = items_to_be_repost[index].get("warehouse") + with repost_gate(item_code, warehouse): + obj = update_entries_after( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": items_to_be_repost[index].get("posting_date"), + "posting_time": items_to_be_repost[index].get("posting_time"), + "creation": items_to_be_repost[index].get("creation"), + "current_idx": index, + "items_to_be_repost": items_to_be_repost, + "repost_doc": doc, + "repost_affected_transaction": repost_affected_transaction, + "item_wh_wise_last_posted_sle": resume_item_wh_wise_last_posted_sle, + }, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) index += 1 diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index 0a084146d29..123ec24e6b4 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -1,4 +1,78 @@ { + "allowed_users": [ + { + "user": "Administrator" + }, + { + "user": "Guest" + }, + { + "user": "accounts@test.com" + }, + { + "user": "ankush@erpnext.com" + }, + { + "user": "faris@erpnext.com" + }, + { + "user": "mention_test_user@example.com" + }, + { + "user": "project@frappe.io" + }, + { + "user": "rushabh@erpnext.com" + }, + { + "user": "saqib@erpnext.com" + }, + { + "user": "soham@frappe.io" + }, + { + "user": "sohamengineer123@gmail.com" + }, + { + "user": "sohamkulkarns9@gmail.com" + }, + { + "user": "stock@xyz.com" + }, + { + "user": "sydel@frappe.io" + }, + { + "user": "test'5@example.com" + }, + { + "user": "test1@example.com" + }, + { + "user": "test2@example.com" + }, + { + "user": "test3@example.com" + }, + { + "user": "test4@example.com" + }, + { + "user": "test@example.com" + }, + { + "user": "test@portal.com" + }, + { + "user": "testpassword@example.com" + }, + { + "user": "testperm@example.com" + }, + { + "user": "web@web.com" + } + ], "app": "erpnext", "charts": [ { @@ -789,9 +863,10 @@ "type": "Link" } ], - "modified": "2026-01-02 12:38:50.043198", + "modified": "2026-06-17 12:11:34.739020", "modified_by": "Administrator", "module": "Stock", + "module_onboarding": "Stock Onboarding", "name": "Stock", "number_cards": [ { @@ -815,6 +890,737 @@ "roles": [], "sequence_id": 7.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Stock", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "chart", + "indent": 0, + "keep_closed": 0, + "label": "Dashboard", + "link_to": "Stock", + "link_type": "Dashboard", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "stock", + "indent": 0, + "keep_closed": 0, + "label": "Stock Entry", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "receipt-text", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Receipt", + "link_to": "Purchase Receipt", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "truck", + "indent": 0, + "keep_closed": 0, + "label": "Delivery Note", + "link_to": "Delivery Note", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "arrow-left-to-line", + "indent": 0, + "keep_closed": 0, + "label": "Material Request", + "link_to": "Material Request", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "caravan", + "indent": 0, + "keep_closed": 0, + "label": "Pick List", + "link_to": "Pick List", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "tool", + "indent": 1, + "keep_closed": 1, + "label": "Tools", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Stock Reconciliation", + "link_to": "Stock Reconciliation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Landed Cost Voucher", + "link_to": "Landed Cost Voucher", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Repost Item Valuation", + "link_to": "Repost Item Valuation", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Packing Slip", + "link_to": "Packing Slip", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection", + "link_to": "Quality Inspection", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_to": "", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item Group", + "link_to": "Item Group", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item Attribute", + "link_to": "Item Attribute", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Brand", + "link_to": "Brand", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Warehouse", + "link_to": "Warehouse", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Unit of Measure (UOM)", + "link_to": "UOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "UOM Conversion Factor", + "link_to": "UOM Conversion Factor", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Serial No", + "link_to": "Serial No", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Batch No", + "link_to": "Batch", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Serial and Batch Bundle", + "link_to": "Serial and Batch Bundle", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Inventory Dimension", + "link_to": "Inventory Dimension", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Shipping Rule", + "link_to": "Shipping Rule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item Alternative", + "link_to": "Item Alternative", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Quality Inspection Template", + "link_to": "Quality Inspection Template", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Trip", + "link_to": "Delivery Trip", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "sheet", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Stock Ledger", + "link_to": "Stock Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Stock Balance", + "link_to": "Stock Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Quick Stock Balance", + "link_to": "Quick Stock Balance", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Stock Projected Qty", + "link_to": "Stock Projected Qty", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Stock Analytics", + "link_to": "Stock Analytics", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Stock Ageing", + "link_to": "Stock Ageing", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Purchase Receipt Trends", + "link_to": "Purchase Receipt Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Note Trends", + "link_to": "Delivery Note Trends", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item Price Stock", + "link_to": "Item Price Stock", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Warehouse Wise Stock Balance", + "link_to": "Warehouse Wise Stock Balance", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item Shortage Report", + "link_to": "Item Shortage Report", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Serial No and Batch Traceability", + "link_to": "Serial No and Batch Traceability", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Serial No Status", + "link_to": "Serial No Status", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Serial No Ledger", + "link_to": "Serial No Ledger", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Serial No Warranty Expiry", + "link_to": "Serial No Warranty Expiry", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Batch-Wise Balance History", + "link_to": "Batch-Wise Balance History", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Batch Item Expiry Status", + "link_to": "Batch Item Expiry Status", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Requested Items To Be Transferred", + "link_to": "Requested Items To Be Transferred", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Itemwise Recommended Reorder Level", + "link_to": "Itemwise Recommended Reorder Level", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item Variant Details", + "link_to": "Item Variant Details", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "default_workspace": 0, + "icon": "settings", + "indent": 1, + "keep_closed": 1, + "label": "Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Stock Settings", + "link_to": "Stock Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Item Variant Settings", + "link_to": "Item Variant Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Stock Reposting Settings", + "link_to": "Stock Reposting Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "default_workspace": 0, + "indent": 0, + "keep_closed": 0, + "label": "Delivery Settings", + "link_to": "Delivery Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Stock", "type": "Workspace" } diff --git a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json b/erpnext/subcontracting/workspace/subcontracting/subcontracting.json index f0d703e0798..1044614fe25 100644 --- a/erpnext/subcontracting/workspace/subcontracting/subcontracting.json +++ b/erpnext/subcontracting/workspace/subcontracting/subcontracting.json @@ -13,7 +13,7 @@ "doctype": "Workspace", "for_user": "", "hide_custom": 0, - "icon": "organization", + "icon": "getting-started", "idx": 2, "is_hidden": 0, "label": "Subcontracting", @@ -138,9 +138,10 @@ "type": "Link" } ], - "modified": "2025-12-19 16:50:25.976741", + "modified": "2026-06-14 13:43:50.289920", "modified_by": "Administrator", "module": "Subcontracting", + "module_onboarding": "Subcontracting Onboarding", "name": "Subcontracting", "number_cards": [ { @@ -164,6 +165,251 @@ "roles": [], "sequence_id": 8.0, "shortcuts": [], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Subcontracting", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "folder-tree", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting BOM", + "link_to": "Subcontracting BOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "move-horizontal", + "indent": 0, + "keep_closed": 0, + "label": "Stock Entry", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-left-to-line", + "indent": 1, + "keep_closed": 0, + "label": "Inward Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Sales Order", + "link_to": "Sales Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Inward Order", + "link_to": "Subcontracting Inward Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Delivery", + "link_to": "Stock Entry", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "arrow-right-from-line", + "indent": 1, + "keep_closed": 0, + "label": "Outward Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Purchase Order", + "link_to": "Purchase Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Order", + "link_to": "Subcontracting Order", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontracting Receipt", + "link_to": "Subcontracting Receipt", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 0, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Bill of Materials", + "link_to": "BOM", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Subcontract Order Summary", + "link_to": "Subcontract Order Summary", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Materials To Be Transferred", + "link_to": "Subcontracted Raw Materials To Be Transferred", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "icon": "", + "indent": 0, + "keep_closed": 0, + "label": "Items To Be Received", + "link_to": "Subcontracted Item To Be Received", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Buying Settings", + "link_type": "DocType", + "navigate_to_tab": "subcontract", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Subcontracting", "type": "Workspace" } diff --git a/erpnext/support/workspace/support/support.json b/erpnext/support/workspace/support/support.json index 8c6647b8d55..900b9b42001 100644 --- a/erpnext/support/workspace/support/support.json +++ b/erpnext/support/workspace/support/support.json @@ -1,7 +1,7 @@ { "app": "erpnext", "charts": [], - "content": "[{\"id\":\"HOEnlt9aR9\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe Helpdesk instead.\",\"col\":12}},{\"id\":\"oxhWhXp9b2\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"Ff8Ab3nLLN\",\"type\":\"card\",\"data\":{\"card_name\":\"Issues\",\"col\":4}},{\"id\":\"_lndiuJTVP\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"id\":\"R_aNO5ESzJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Service Level Agreement\",\"col\":4}},{\"id\":\"N8aA2afWfi\",\"type\":\"card\",\"data\":{\"card_name\":\"Warranty\",\"col\":4}},{\"id\":\"M5fxGuFwUR\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"xKH0kO9q4P\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "content": "[{\"id\":\"HOEnlt9aR9\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use Frappe Helpdesk instead.\",\"col\":12}},{\"id\":\"qzP2mZrGOu\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"Fkdjo6bJ7A\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Issue\",\"col\":3}},{\"id\":\"OTS8kx2f3x\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Maintenance Visit\",\"col\":3}},{\"id\":\"smDTSjBR3Z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Service Level Agreement\",\"col\":3}},{\"id\":\"WCqL_gBYGU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"oxhWhXp9b2\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"Ff8Ab3nLLN\",\"type\":\"card\",\"data\":{\"card_name\":\"Issues\",\"col\":4}},{\"id\":\"_lndiuJTVP\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"id\":\"R_aNO5ESzJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Service Level Agreement\",\"col\":4}},{\"id\":\"N8aA2afWfi\",\"type\":\"card\",\"data\":{\"card_name\":\"Warranty\",\"col\":4}},{\"id\":\"M5fxGuFwUR\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"xKH0kO9q4P\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-02 15:48:23.224699", "custom_blocks": [], "docstatus": 0, @@ -172,7 +172,7 @@ "type": "Link" } ], - "modified": "2026-01-02 17:45:04.203273", + "modified": "2026-06-14 13:44:07.764547", "modified_by": "Administrator", "module": "Support", "name": "Support", @@ -184,7 +184,179 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 12.0, - "shortcuts": [], + "shortcuts": [ + { + "color": "Yellow", + "format": "{} Assigned", + "label": "Issue", + "link_to": "Issue", + "stats_filter": "{\n \"_assign\": [\"like\", '%' + frappe.session.user + '%'],\n \"status\": \"Open\"\n}", + "type": "DocType" + }, + { + "label": "Maintenance Visit", + "link_to": "Maintenance Visit", + "type": "DocType" + }, + { + "label": "Service Level Agreement", + "link_to": "Service Level Agreement", + "type": "DocType" + } + ], + "sidebar_items": [ + { + "child": 0, + "collapsible": 1, + "icon": "home", + "indent": 0, + "keep_closed": 0, + "label": "Home", + "link_to": "Support", + "link_type": "Workspace", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "file-question-mark", + "indent": 0, + "keep_closed": 0, + "label": "Issue", + "link_to": "Issue", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "calendar-days", + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Schedule", + "link_to": "Maintenance Schedule", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "calendar-check-2", + "indent": 0, + "keep_closed": 0, + "label": "Maintenance Visit", + "link_to": "Maintenance Visit", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "grid-2x2-check", + "indent": 0, + "keep_closed": 0, + "label": "Warranty Claim", + "link_to": "Warranty Claim", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "database", + "indent": 1, + "keep_closed": 1, + "label": "Setup", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Issue Type", + "link_to": "Issue Type", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Issue Priority", + "link_to": "Issue Priority", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "Service Level Agreement", + "link_to": "Service Level Agreement", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "notepad-text", + "indent": 1, + "keep_closed": 1, + "label": "Reports", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Section Break" + }, + { + "child": 1, + "collapsible": 1, + "indent": 0, + "keep_closed": 0, + "label": "First Response Time for Issues", + "link_to": "First Response Time for Issues", + "link_type": "Report", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + }, + { + "child": 0, + "collapsible": 1, + "icon": "settings", + "indent": 0, + "keep_closed": 0, + "label": "Settings", + "link_to": "Support Settings", + "link_type": "DocType", + "open_in_new_tab": 0, + "show_arrow": 0, + "type": "Link" + } + ], + "standard": 1, "title": "Support", "type": "Workspace" } diff --git a/erpnext/workspace_sidebar/accounts_setup.json b/erpnext/workspace_sidebar/accounts_setup.json index df28f57238a..93a436ee15b 100644 --- a/erpnext/workspace_sidebar/accounts_setup.json +++ b/erpnext/workspace_sidebar/accounts_setup.json @@ -14,6 +14,7 @@ "keep_closed": 0, "label": "Setup", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -25,6 +26,7 @@ "label": "Chart of Accounts", "link_to": "Account", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -36,6 +38,7 @@ "label": "Chart of Cost Centers", "link_to": "Cost Center", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -47,6 +50,7 @@ "label": "Account Category", "link_to": "Account Category", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -58,6 +62,7 @@ "label": "Accounting Dimension", "link_to": "Accounting Dimension", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -69,6 +74,7 @@ "label": "Currency", "link_to": "Currency", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -80,6 +86,7 @@ "label": "Currency Exchange", "link_to": "Currency Exchange", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -91,6 +98,7 @@ "label": "Finance Book", "link_to": "Finance Book", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -102,6 +110,7 @@ "label": "Mode of Payment", "link_to": "Mode of Payment", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -113,6 +122,7 @@ "label": "Payment Term", "link_to": "Payment Term", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -124,6 +134,7 @@ "label": "Journal Entry Template", "link_to": "Journal Entry Template", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -135,6 +146,7 @@ "label": "Terms and Conditions", "link_to": "Terms and Conditions", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -146,6 +158,7 @@ "label": "Company", "link_to": "Company", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -157,6 +170,7 @@ "label": "Fiscal Year", "link_to": "Fiscal Year", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -168,6 +182,7 @@ "label": "Sales Taxes", "link_to": "Sales Taxes and Charges Template", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -179,6 +194,7 @@ "keep_closed": 0, "label": "Opening & Closing", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -191,6 +207,7 @@ "label": "COA Importer", "link_to": "Chart of Accounts Importer", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -203,6 +220,7 @@ "label": "Opening Invoice Tool", "link_to": "Opening Invoice Creation Tool", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -215,6 +233,7 @@ "label": "Accounting Period", "link_to": "Accounting Period", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -227,6 +246,7 @@ "label": "FX Revaluation", "link_to": "Exchange Rate Revaluation", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -239,6 +259,7 @@ "label": "Period Closing Voucher", "link_to": "Period Closing Voucher", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -250,6 +271,7 @@ "keep_closed": 0, "label": "Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -262,6 +284,7 @@ "label": "Accounts Settings", "link_to": "Accounts Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -273,22 +296,12 @@ "label": "Currency Exchange Settings", "link_to": "Currency Exchange Settings", "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Repost Accounting Ledger Settings", - "link_to": "Repost Accounting Ledger Settings", - "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } ], - "modified": "2026-02-23 22:20:51.043478", + "modified": "2026-06-12 14:50:50.262533", "modified_by": "Administrator", "module": "Accounts", "module_onboarding": "Accounting Onboarding", diff --git a/erpnext/workspace_sidebar/budget.json b/erpnext/workspace_sidebar/budgeting.json similarity index 89% rename from erpnext/workspace_sidebar/budget.json rename to erpnext/workspace_sidebar/budgeting.json index dd9b6f87311..e98e6ca36ce 100644 --- a/erpnext/workspace_sidebar/budget.json +++ b/erpnext/workspace_sidebar/budgeting.json @@ -15,6 +15,7 @@ "label": "Budget", "link_to": "Budget", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -27,6 +28,7 @@ "label": "Cost Center", "link_to": "Cost Center", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -39,6 +41,7 @@ "label": "Accounting Dimension", "link_to": "Accounting Dimension", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -51,6 +54,7 @@ "label": "Cost Center Allocation", "link_to": "Cost Center Allocation", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -63,6 +67,7 @@ "label": "Budget Variance", "link_to": "Budget Variance Report", "link_type": "Report", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } @@ -70,8 +75,8 @@ "modified": "2026-01-10 00:06:13.032297", "modified_by": "Administrator", "module": "Accounts", - "name": "Budget", + "name": "Budgeting", "owner": "Administrator", "standard": 1, - "title": "Budget" + "title": "Budgeting" } diff --git a/erpnext/workspace_sidebar/erpnext_settings.json b/erpnext/workspace_sidebar/erpnext_settings.json index 117a36b666e..c89270ce56f 100644 --- a/erpnext/workspace_sidebar/erpnext_settings.json +++ b/erpnext/workspace_sidebar/erpnext_settings.json @@ -15,6 +15,7 @@ "label": "Global Defaults", "link_to": "Global Defaults", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -27,6 +28,7 @@ "label": "System Settings", "link_to": "System Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -39,6 +41,7 @@ "label": "Accounts Settings", "link_to": "Accounts Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -51,6 +54,7 @@ "label": "POS Settings", "link_to": "POS Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -63,6 +67,7 @@ "label": "Selling Settings", "link_to": "Selling Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -75,6 +80,7 @@ "label": "Buying Settings", "link_to": "Buying Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -87,6 +93,7 @@ "label": "Stock Settings", "link_to": "Stock Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -99,6 +106,7 @@ "label": "Manufacturing Settings", "link_to": "Manufacturing Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -111,6 +119,7 @@ "label": "Projects Settings", "link_to": "Projects Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -123,6 +132,7 @@ "label": "CRM Settings", "link_to": "CRM Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -135,6 +145,7 @@ "label": "Support Settings", "link_to": "Support Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -146,6 +157,7 @@ "keep_closed": 1, "label": "Other Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -157,6 +169,7 @@ "label": "Subscription Settings", "link_to": "Subscription Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -168,6 +181,7 @@ "label": "Item Variant Settings", "link_to": "Item Variant Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -179,6 +193,7 @@ "label": "Delivery Settings", "link_to": "Delivery Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -190,6 +205,7 @@ "label": "Currency Exchange Settings", "link_to": "Currency Exchange Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -201,6 +217,7 @@ "label": "Appointment Booking Settings", "link_to": "Appointment Booking Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -212,22 +229,12 @@ "label": "Stock Reposting Settings", "link_to": "Stock Reposting Settings", "link_type": "DocType", - "show_arrow": 0, - "type": "Link" - }, - { - "child": 1, - "collapsible": 1, - "indent": 0, - "keep_closed": 0, - "label": "Repost Accounting Ledger Settings", - "link_to": "Repost Accounting Ledger Settings", - "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } ], - "modified": "2026-01-10 00:06:12.956275", + "modified": "2026-06-12 14:51:11.333051", "modified_by": "Administrator", "module": "Setup", "name": "ERPNext Settings", diff --git a/erpnext/workspace_sidebar/organization.json b/erpnext/workspace_sidebar/organization.json index 8ea0a44faca..06802556abe 100644 --- a/erpnext/workspace_sidebar/organization.json +++ b/erpnext/workspace_sidebar/organization.json @@ -9,89 +9,103 @@ { "child": 0, "collapsible": 1, + "default_workspace": 1, "icon": "organization", "indent": 0, "keep_closed": 0, "label": "Company", "link_to": "Company", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "book-text", "indent": 0, "keep_closed": 0, "label": "Letter Head", "link_to": "Letter Head", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "file-user", "indent": 0, "keep_closed": 0, "label": "Department", "link_to": "Department", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "book-user", "indent": 0, "keep_closed": 0, "label": "Branch", "link_to": "Branch", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "users", "indent": 0, "keep_closed": 0, "label": "User", "link_to": "User", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "user-round-check", "indent": 0, "keep_closed": 0, "label": "Role Permissions", "link_to": "permission-manager", "link_type": "Page", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, { "child": 0, "collapsible": 1, + "default_workspace": 0, "icon": "mail", "indent": 0, "keep_closed": 0, "label": "Email Account", "link_to": "Email Account", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } ], - "modified": "2026-02-24 18:08:00.796746", + "modified": "2026-06-16 00:37:22.942285", "modified_by": "Administrator", "module": "Setup", "module_onboarding": "Organization Onboarding", diff --git a/erpnext/workspace_sidebar/subscription.json b/erpnext/workspace_sidebar/subscriptions.json similarity index 88% rename from erpnext/workspace_sidebar/subscription.json rename to erpnext/workspace_sidebar/subscriptions.json index ca42736b27c..ec188edf169 100644 --- a/erpnext/workspace_sidebar/subscription.json +++ b/erpnext/workspace_sidebar/subscriptions.json @@ -15,6 +15,7 @@ "label": "Subscription", "link_to": "Subscription", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -27,6 +28,7 @@ "label": "Subscription Plan", "link_to": "Subscription Plan", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -39,6 +41,7 @@ "label": "Subscription Settings", "link_to": "Subscription Settings", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -50,6 +53,7 @@ "keep_closed": 1, "label": "Setup", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Section Break" }, @@ -61,6 +65,7 @@ "label": "Customer", "link_to": "Customer", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -72,6 +77,7 @@ "label": "Supplier", "link_to": "Supplier", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" }, @@ -83,6 +89,7 @@ "label": "Item", "link_to": "Item", "link_type": "DocType", + "open_in_new_tab": 0, "show_arrow": 0, "type": "Link" } @@ -90,8 +97,8 @@ "modified": "2026-01-10 00:06:13.048591", "modified_by": "Administrator", "module": "Accounts", - "name": "Subscription", + "name": "Subscriptions", "owner": "Administrator", "standard": 1, - "title": "Subscription" + "title": "Subscriptions" }