diff --git a/erpnext/accounts/dashboard_chart/bank_balance/bank_balance.json b/erpnext/accounts/dashboard_chart/bank_balance/bank_balance.json index 6442c022c73..b0c3a7163b5 100644 --- a/erpnext/accounts/dashboard_chart/bank_balance/bank_balance.json +++ b/erpnext/accounts/dashboard_chart/bank_balance/bank_balance.json @@ -9,18 +9,20 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 12:19:59.879476", - "modified": "2020-07-22 12:21:48.780513", + "last_synced_on": "2026-01-02 13:01:24.037552", + "modified": "2026-01-02 13:04:57.850305", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Balance", "number_of_groups": 0, "owner": "Administrator", + "roles": [], + "show_values_over_chart": 1, "source": "Account Balance Timeline", - "time_interval": "Quarterly", - "timeseries": 0, + "time_interval": "Monthly", + "timeseries": 1, "timespan": "Last Year", "type": "Line", "use_report_chart": 0, "y_axis": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json index 25caa44769b..01701ff7e3a 100644 --- a/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json +++ b/erpnext/accounts/dashboard_chart/profit_and_loss/profit_and_loss.json @@ -1,7 +1,7 @@ { "chart_name": "Profit and Loss", "chart_type": "Report", - "creation": "2020-07-17 11:25:34.448572", + "creation": "2025-04-01 20:38:16.986176", "docstatus": 0, "doctype": "Dashboard Chart", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", @@ -9,7 +9,7 @@ "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2023-07-19 13:08:56.470390", + "modified": "2025-12-19 12:37:31.673782", "modified_by": "Administrator", "module": "Accounts", "name": "Profit and Loss", @@ -17,8 +17,9 @@ "owner": "Administrator", "report_name": "Profit and Loss Statement", "roles": [], + "show_values_over_chart": 1, "timeseries": 0, - "type": "Bar", + "type": "Line", "use_report_chart": 1, "y_axis": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index be723915951..c3e23fe6e50 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -450,14 +450,12 @@ def process_deferred_accounting(posting_date=None): for company in companies: for record_type in ("Income", "Expense"): doc = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - company=company.name, - posting_date=posting_date, - start_date=start_date, - end_date=end_date, - type=record_type, - ) + doctype="Process Deferred Accounting", + company=company.name, + posting_date=posting_date, + start_date=start_date, + end_date=end_date, + type=record_type, ) doc.insert() diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 795ee006806..6deff1ac3e6 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -415,15 +415,13 @@ def create_account(**kwargs): return account.name else: account = frappe.get_doc( - dict( - doctype="Account", - is_group=kwargs.get("is_group", 0), - account_name=kwargs.get("account_name"), - account_type=kwargs.get("account_type"), - parent_account=kwargs.get("parent_account"), - company=kwargs.get("company"), - account_currency=kwargs.get("account_currency"), - ) + doctype="Account", + is_group=kwargs.get("is_group", 0), + account_name=kwargs.get("account_name"), + account_type=kwargs.get("account_type"), + parent_account=kwargs.get("parent_account"), + company=kwargs.get("company"), + account_currency=kwargs.get("account_currency"), ) account.save() diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index cf1d52b086b..1dde96c223d 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -37,6 +37,59 @@ class TestAccountingPeriod(IntegrationTestCase): doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC") self.assertRaises(ClosedAccountingPeriod, doc.save) + def test_accounting_period_exempted_role(self): + # Create Accounting Period with exempted role + ap = create_accounting_period( + period_name="Test Accounting Period Exempted", + exempted_role="Accounts Manager", + start_date="2025-12-01", + end_date="2025-12-31", + ) + ap.save() + + # Create users + users = frappe.get_all("User", filters={"email": ["like", "test%"]}, limit=1) + user = None + + if users[0].name: + user = frappe.get_doc("User", users[0].name) + else: + user = frappe.get_doc( + { + "doctype": "User", + "email": "test1@example.com", + "first_name": "Test1", + } + ) + user.insert() + + user.roles = [] + user.append("roles", {"role": "Accounts User"}) + + # ---- Non-exempted user should FAIL ---- + user.save(ignore_permissions=True) + frappe.clear_cache(user=user.name) + + frappe.set_user(user.name) + posting_date = "2025-12-11" + doc = create_sales_invoice( + do_not_save=1, + posting_date=posting_date, + ) + + with self.assertRaises(frappe.ValidationError): + doc.submit() + + # ---- Exempted role should PASS ---- + user.append("roles", {"role": "Accounts Manager"}) + user.save(ignore_permissions=True) + frappe.clear_cache(user=user.name) + + doc = create_sales_invoice(do_not_save=1, posting_date=posting_date) + + doc.submit() # Should not raise + self.assertEqual(doc.docstatus, 1) + def tearDown(self): for d in frappe.get_all("Accounting Period"): frappe.delete_doc("Accounting Period", d.name) @@ -51,5 +104,6 @@ def create_accounting_period(**args): accounting_period.company = args.company or "_Test Company" accounting_period.period_name = args.period_name or "_Test_Period_Name_1" accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1}) + accounting_period.exempted_role = args.exempted_role or "" return accounting_period diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index dc657945544..55db06f8ca7 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -91,6 +91,7 @@ "receivable_payable_remarks_length", "accounts_receivable_payable_tuning_section", "receivable_payable_fetch_method", + "default_ageing_range", "column_break_ntmi", "drop_ar_procedures", "legacy_section", @@ -649,15 +650,22 @@ "fieldtype": "Link", "label": "Role to Notify on Depreciation Failure", "options": "Role" + }, + { + "default": "30, 60, 90, 120", + "fieldname": "default_ageing_range", + "fieldtype": "Data", + "label": "Default Ageing Range" } ], "grid_page_length": 50, + "hide_toolbar": 1, "icon": "icon-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-12-03 20:42:13.238050", + "modified": "2026-01-02 18:17:18.994348", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 557afd02a48..73d51000a5b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -40,6 +40,7 @@ class AccountsSettings(Document): confirm_before_resetting_posting_date: DF.Check create_pr_in_draft_status: DF.Check credit_controller: DF.Link | None + default_ageing_range: DF.Data | None delete_linked_ledger_entries: DF.Check determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] enable_common_party_accounting: DF.Check diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 1d96c979d1d..231e9cdf522 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -125,7 +125,7 @@ class BankClearance(Document): ) msg += "" - frappe.throw(_(msg)) + msgprint(_(msg)) return if not entries_to_update: @@ -134,16 +134,44 @@ class BankClearance(Document): for d in entries_to_update: if d.payment_document == "Sales Invoice": - frappe.db.set_value( + old_clearance_date = frappe.db.get_value( "Sales Invoice Payment", - {"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]}, + { + "parent": d.payment_entry, + "account": self.account, + "amount": [">", 0], + }, "clearance_date", - d.clearance_date, ) + if d.clearance_date or old_clearance_date: + frappe.db.set_value( + "Sales Invoice Payment", + {"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]}, + "clearance_date", + d.clearance_date, + ) + sales_invoice = frappe.get_lazy_doc("Sales Invoice", d.payment_entry) + sales_invoice.add_comment( + "Comment", + _("Clearance date changed from {0} to {1} via Bank Clearance Tool").format( + old_clearance_date, d.clearance_date + ), + ) + else: - # using db_set to trigger notification payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry) - payment_entry.db_set("clearance_date", d.clearance_date) + old_clearance_date = payment_entry.clearance_date + + if d.clearance_date or old_clearance_date: + # using db_set to trigger notification + payment_entry.db_set("clearance_date", d.clearance_date) + + payment_entry.add_comment( + "Comment", + _("Clearance date changed from {0} to {1} via Bank Clearance Tool").format( + old_clearance_date, d.clearance_date + ), + ) self.get_payment_entries() msgprint(_("Clearance Date updated")) diff --git a/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.json b/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.json index 194c16d807f..9e98bfd2443 100644 --- a/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.json +++ b/erpnext/accounts/doctype/bank_clearance_detail/bank_clearance_detail.json @@ -30,8 +30,7 @@ "label": "Payment Entry", "oldfieldname": "voucher_id", "oldfieldtype": "Link", - "options": "payment_document", - "width": "50" + "options": "payment_document" }, { "columns": 2, @@ -69,7 +68,7 @@ "read_only": 1 }, { - "columns": 2, + "columns": 1, "fieldname": "cheque_number", "fieldtype": "Data", "in_list_view": 1, @@ -79,8 +78,10 @@ "read_only": 1 }, { + "columns": 2, "fieldname": "cheque_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Cheque Date", "oldfieldname": "cheque_date", "oldfieldtype": "Date", @@ -96,17 +97,19 @@ "oldfieldtype": "Date" } ], + "grid_page_length": 50, "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:06:37.609319", + "modified": "2025-12-17 14:33:45.913311", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Clearance Detail", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 2c8d011853f..2fd49c962f7 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -304,6 +304,7 @@ def create_payment_entry_bts( project=None, cost_center=None, allow_edit=None, + company_bank_account=None, ): # Create a new payment entry based on the bank transaction bank_transaction = frappe.db.get_values( @@ -345,6 +346,9 @@ def create_payment_entry_bts( pe.project = project pe.cost_center = cost_center + if company_bank_account: + pe.bank_account = company_bank_account + pe.validate() if allow_edit: diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index e40749fdd5d..f850749fe4f 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -50,6 +50,9 @@ class BankTransaction(Document): self.handle_excluded_fee() self.update_allocated_amount() + def on_discard(self): + self.db_set("status", "Cancelled") + def validate(self): self.validate_included_fee() self.validate_duplicate_references() diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 614f4e6d3e5..ae3dbd59d69 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -101,10 +101,11 @@ "label": "Use HTTP Protocol" } ], + "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-11-25 13:03:41.896424", + "modified": "2026-01-02 18:19:02.873815", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py index 67fcebb7ebf..4de556b7e46 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py @@ -71,7 +71,9 @@ class PeriodValue: class AccountData: """Account data across all periods""" - account_name: str + account: str # docname + account_name: str = "" # account name + account_number: str = "" period_values: dict[str, PeriodValue] = field(default_factory=dict) def add_period(self, period_value: PeriodValue) -> None: @@ -103,7 +105,11 @@ class AccountData: # movement is unaccumulated by default def copy(self): - copied = AccountData(account_name=self.account_name) + copied = AccountData( + account=self.account, + account_name=self.account_name, + account_number=self.account_number, + ) copied.period_values = {k: v.copy() for k, v in self.period_values.items()} return copied @@ -329,12 +335,10 @@ class DataCollector: self.account_fields = {field.fieldname for field in frappe.get_meta("Account").fields} def add_account_request(self, row): - accounts = self._parse_account_filter(self.company, row) - self.account_requests.append( { "row": row, - "accounts": accounts, + "accounts": self._parse_account_filter(self.company, row), "balance_type": row.balance_type, "reference_code": row.reference_code, "reverse_sign": row.reverse_sign, @@ -345,12 +349,12 @@ class DataCollector: if not self.account_requests: return {"account_data": {}, "summary": {}, "account_details": {}} - # Get all unique accounts - all_accounts = set() - for request in self.account_requests: - all_accounts.update(request["accounts"]) + # Get all accounts + all_accounts = [] + + for request in self.account_requests: + all_accounts.extend(request["accounts"]) - all_accounts = list(all_accounts) if not all_accounts: return {"account_data": {}, "summary": {}, "account_details": {}} @@ -373,7 +377,9 @@ class DataCollector: total_values = [0.0] * len(self.periods) request_account_details = {} - for account_name in accounts: + for account in accounts: + account_name = account.name + if account_name not in account_data: continue @@ -396,20 +402,21 @@ class DataCollector: return {"account_data": account_data, "summary": summary, "account_details": account_details} @staticmethod - def _parse_account_filter(company, report_row) -> list[str]: + def _parse_account_filter(company, report_row) -> list[dict]: """ Find accounts matching filter criteria. Example: - Input: '["account_type", "=", "Cash"]' - Output: ["Cash - COMP", "Petty Cash - COMP", "Bank - COMP"] + + - Input: '["account_type", "=", "Cash"]' + - Output: [{"name": "Cash - COMP", "account_name": "Cash", "account_number": "1001"}] """ filter_parser = FilterExpressionParser() account = frappe.qb.DocType("Account") query = ( frappe.qb.from_(account) - .select(account.name) + .select(account.name, account.account_name, account.account_number) .where(account.disabled == 0) .where(account.is_group == 0) ) @@ -423,8 +430,8 @@ class DataCollector: query = query.where(where_condition) query = query.orderby(account.name) - result = query.run(as_dict=True) - return [row.name for row in result] + + return query.run(as_dict=True) @staticmethod def get_filtered_accounts(company: str, account_rows: list) -> list[str]: @@ -456,17 +463,35 @@ class FinancialQueryBuilder: self.filters = filters self.periods = periods self.company = filters.get("company") + self.account_meta = {} # {name: {account_name, account_number}} - def fetch_account_balances(self, accounts: list[str]) -> dict[str, AccountData]: + def fetch_account_balances(self, accounts: list[dict]) -> dict[str, AccountData]: """ Fetch account balances for all periods with optimization. Steps: get opening balances → fetch GL entries → calculate running totals + - accounts: list of accounts with details + + ``` + { + "name": "Cash - COMP", + "account_name": "Cash", + "account_number": "1001", + } + ``` + Returns: dict: {account: AccountData} """ - balances_data = self._get_opening_balances(accounts) - gl_data = self._get_gl_movements(accounts) + account_names = list({acc.name for acc in accounts}) + # NOTE: do not change accounts list as it is used in caller function + self.account_meta = { + acc.name: {"account_name": acc.account_name, "account_number": acc.account_number} + for acc in accounts + } + + balances_data = self._get_opening_balances(account_names) + gl_data = self._get_gl_movements(account_names) self._calculate_running_balances(balances_data, gl_data) self._handle_balance_accumulation(balances_data) @@ -543,7 +568,8 @@ class FinancialQueryBuilder: gap_movement = gap_movements.get(account, 0.0) opening_balance = closing_balance + gap_movement - account_data = AccountData(account) + account_data = AccountData(account=account, **self._get_account_meta(account)) + account_data.add_period(PeriodValue(first_period_key, opening_balance, 0, 0)) balances_data[account] = account_data @@ -613,7 +639,7 @@ class FinancialQueryBuilder: for row in gl_data: account = row["account"] if account not in balances_data: - balances_data[account] = AccountData(account) + balances_data[account] = AccountData(account=account, **self._get_account_meta(account)) account_data: AccountData = balances_data[account] @@ -714,6 +740,9 @@ class FinancialQueryBuilder: return query.run(as_dict=True) + def _get_account_meta(self, account: str) -> dict[str, Any]: + return self.account_meta.get(account, {}) + class FilterExpressionParser: """Direct filter expression to SQL condition builder""" @@ -1544,20 +1573,29 @@ class RowFormatterBase(ABC): pass def _get_values(self, row_data: RowData) -> dict[str, Any]: - # TODO: can be commonify COA? @abdeali + def _get_row_data(key: str, default: Any = "") -> Any: + return getattr(row_data.row, key, default) or default + + def _get_filter_value(key: str, default: Any = "") -> Any: + return getattr(self.context.filters, key, default) or default + child_accounts = [] if row_data.account_details: child_accounts = list(row_data.account_details.keys()) + display_name = _get_row_data("display_name", "") + values = { + "account": _get_row_data("account", "") or display_name, + "account_name": display_name, + "acc_name": _get_row_data("account_name", ""), + "acc_number": _get_row_data("account_number", ""), "child_accounts": child_accounts, - "account": getattr(row_data.row, "display_name", "") or "", - "indent": getattr(row_data.row, "indentation_level", 0), - "account_name": getattr(row_data.row, "account", "") or "", "currency": self.context.currency or "", - "period_start_date": getattr(self.context.filters, "period_start_date", "") or "", - "period_end_date": getattr(self.context.filters, "period_end_date", "") or "", + "indent": _get_row_data("indentation_level", 0), + "period_start_date": _get_filter_value("period_start_date", ""), + "period_end_date": _get_filter_value("period_end_date", ""), "total": 0, } @@ -1670,8 +1708,8 @@ class DetailRowBuilder: detail_rows = [] parent_row = self.parent_row_data.row - for account_name, account_data in self.parent_row_data.account_details.items(): - detail_row = self._create_detail_row_object(account_name, parent_row) + for account_data in self.parent_row_data.account_details.values(): + detail_row = self._create_detail_row_object(account_data, parent_row) balance_type = getattr(parent_row, "balance_type", "Closing Balance") values = account_data.get_values_by_type(balance_type) @@ -1687,16 +1725,20 @@ class DetailRowBuilder: return detail_rows - def _create_detail_row_object(self, account_name: str, parent_row): - short_name = account_name.rsplit(" - ", 1)[0].strip() + def _create_detail_row_object(self, account_data: AccountData, parent_row): + acc_name = account_data.account_name or "" + acc_number = account_data.account_number or "" + + display_name = f"{_(acc_number)} - {_(acc_name)}" if acc_number else _(acc_name) return type( "DetailRow", (), { - "display_name": short_name, - "account": account_name, - "account_name": short_name, + "account": account_data.account, + "display_name": display_name, + "account_name": acc_name, + "account_number": acc_number, "data_source": "Account Detail", "indentation_level": getattr(parent_row, "indentation_level", 0) + 1, "fieldtype": getattr(parent_row, "fieldtype", None), diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py index bc966916ef3..ecbde0acdda 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py @@ -4,6 +4,7 @@ from frappe import _ def get_data(): return { "fieldname": "fiscal_year", + "non_standard_fieldnames": {"Budget": "from_fiscal_year"}, "transactions": [ {"label": _("Budgets"), "items": ["Budget"]}, {"label": _("References"), "items": ["Period Closing Voucher"]}, diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index c52c86528fd..3abfa176622 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -193,7 +193,6 @@ class GLEntry(Document): account_type == "Profit and Loss" and self.company == dimension.company and dimension.mandatory_for_pl - and not dimension.disabled and not self.is_cancelled ): if not self.get(dimension.fieldname): @@ -207,7 +206,6 @@ class GLEntry(Document): account_type == "Balance Sheet" and self.company == dimension.company and dimension.mandatory_for_bs - and not dimension.disabled and not self.is_cancelled ): if not self.get(dimension.fieldname): diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js index 0b08e393de2..7626dc8a258 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_list.js @@ -9,8 +9,8 @@ frappe.listview_settings["Invoice Discounting"] = { return [__("Disbursed"), "blue", "status,=,Disbursed"]; } else if (doc.status == "Settled") { return [__("Settled"), "orange", "status,=,Settled"]; - } else if (doc.status == "Canceled") { - return [__("Canceled"), "red", "status,=,Canceled"]; + } else if (doc.status == "Cancelled") { + return [__("Cancelled"), "red", "status,=,Cancelled"]; } }, }; diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 3f772264d78..c115204c28b 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -43,6 +43,20 @@ frappe.ui.form.on("Journal Entry", { }, }; }); + + frm.set_query("project", "accounts", function (doc, cdt, cdn) { + let row = frappe.get_doc(cdt, cdn); + let filters = { + company: doc.company, + }; + if (row.party_type == "Customer") { + filters.customer = row.party; + } + return { + query: "erpnext.controllers.queries.get_project_name", + filters, + }; + }); }, get_balance_for_periodic_accounting(frm) { @@ -112,9 +126,11 @@ frappe.ui.form.on("Journal Entry", { erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); - $.each(frm.doc.accounts || [], function (i, row) { - erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name); - }); + if (frm.doc.voucher_type !== "Exchange Gain Or Loss") { + $.each(frm.doc.accounts || [], function (i, row) { + erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name); + }); + } }, before_save: function (frm) { if (frm.doc.docstatus == 0 && !frm.doc.is_system_generated) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 6bf739c6b5a..93b95d7c02e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -6,6 +6,7 @@ import json import frappe from frappe import _, msgprint, scrub +from frappe.core.doctype.submission_queue.submission_queue import queue_submission from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate import erpnext @@ -179,15 +180,13 @@ class JournalEntry(AccountsController): def submit(self): if len(self.accounts) > 100: - msgprint(_("The task has been enqueued as a background job."), alert=True) - self.queue_action("submit", timeout=4600) + queue_submission(self, "_submit") else: return self._submit() def cancel(self): if len(self.accounts) > 100: - msgprint(_("The task has been enqueued as a background job."), alert=True) - self.queue_action("cancel", timeout=4600) + queue_submission(self, "_cancel") else: return self._cancel() diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js index 25d437f154f..ec53f1fa78e 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", { let d = locals[cdt][cdn]; return { filters: [ - ["Account", "account_type", "in", "Bank, Cash, Receivable"], + ["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]], ["Account", "is_group", "=", 0], ["Account", "company", "=", d.company], ], diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py index ba2cb671d40..921af59b50a 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py @@ -11,6 +11,5 @@ def get_data(): }, "transactions": [ {"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]}, - {"items": ["Budget"]}, ], } diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 40544fe6159..3020e4e6659 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -182,7 +182,7 @@ frappe.ui.form.on("Payment Entry", { "Dunning", ]; - if (in_list(party_type_doctypes, child.reference_doctype)) { + if (party_type_doctypes.includes(child.reference_doctype)) { filters[doc.party_type.toLowerCase()] = doc.party; } @@ -427,7 +427,15 @@ frappe.ui.form.on("Payment Entry", { if (frm.doc.payment_type == "Internal Transfer") { $.each( - ["party", "party_type", "paid_from", "paid_to", "references", "total_allocated_amount"], + [ + "party", + "party_type", + "paid_from", + "paid_to", + "references", + "total_allocated_amount", + "party_name", + ], function (i, field) { frm.set_value(field, null); } @@ -1033,7 +1041,7 @@ frappe.ui.form.on("Payment Entry", { c.allocated_amount = d.allocated_amount; c.account = d.account; - if (!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) { + if (!frm.events.get_order_doctypes(frm).includes(d.voucher_type)) { if (flt(d.outstanding_amount) > 0) total_positive_outstanding += flt(d.outstanding_amount); else total_negative_outstanding += Math.abs(flt(d.outstanding_amount)); @@ -1049,7 +1057,7 @@ frappe.ui.form.on("Payment Entry", { } else { c.exchange_rate = 1; } - if (in_list(frm.events.get_invoice_doctypes(frm), d.reference_doctype)) { + if (frm.events.get_invoice_doctypes(frm).includes(d.reference_doctype)) { c.due_date = d.due_date; } }); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index eb1673a8798..350e8b700a9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1285,8 +1285,11 @@ class PaymentEntry(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gl_entries = self.build_gl_map() - gl_entries = process_gl_map(gl_entries) - make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) + + merge_entries = frappe.get_single_value("Accounts Settings", "merge_similar_account_heads") + + gl_entries = process_gl_map(gl_entries, merge_entries=merge_entries) + make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj, merge_entries=merge_entries) if cancel: cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) else: diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 355aa46ea05..f6c240c0714 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1045,6 +1045,7 @@ class TestPaymentEntry(IntegrationTestCase): ) def test_gl_of_multi_currency_payment_with_taxes(self): + frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1) payment_entry = create_payment_entry( party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True ) @@ -1606,6 +1607,96 @@ class TestPaymentEntry(IntegrationTestCase): self.voucher_no = pe.name self.check_gl_entries() + def test_payment_entry_merges_gl_entries_with_same_account_head(self): + """ + Test that Payment Entry merges GL entries with same account head + when 'Merge Similar Account Heads' setting is enabled. + """ + frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1) + + pe = create_payment_entry( + party_type="Supplier", + party="_Test Supplier", + paid_from="_Test Bank - _TC", + paid_to="Creditors - _TC", + ) + + pe.append( + "deductions", + { + "account": "Write Off - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 50, + }, + ) + + pe.append( + "deductions", + { + "account": "Write Off - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 30, + }, + ) + + pe.save() + pe.submit() + + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pe.name, "account": "Write Off - _TC", "is_cancelled": 0}, + fields=["debit", "credit"], + ) + + self.assertEqual(len(gl_entries), 1) + self.assertEqual(gl_entries[0].debit, 80) + + def test_payment_entry_does_not_merge_gl_entries_when_setting_disabled(self): + """ + Test that Payment Entry does NOT merge GL entries + when 'Merge Similar Account Heads' is disabled. + """ + + frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) + + pe = create_payment_entry( + party_type="Supplier", + party="_Test Supplier", + paid_from="_Test Bank - _TC", + paid_to="Creditors - _TC", + ) + + pe.append( + "deductions", + { + "account": "Write Off - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 50, + }, + ) + + pe.append( + "deductions", + { + "account": "Write Off - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 30, + }, + ) + + pe.save() + pe.submit() + + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pe.name, "account": "Write Off - _TC", "is_cancelled": 0}, + fields=["debit", "credit"], + ) + + self.assertEqual(len(gl_entries), 2) + + frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1) + def check_pl_entries(self): ple = frappe.qb.DocType("Payment Ledger Entry") pl_entries = ( diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 4eeb9a1746b..afb68d8a3ce 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -70,7 +70,7 @@ { "columns": 2, "fieldname": "total_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Grand Total", "print_hide": 1, @@ -79,7 +79,7 @@ { "columns": 2, "fieldname": "outstanding_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Outstanding", "read_only": 1 @@ -87,7 +87,7 @@ { "columns": 2, "fieldname": "allocated_amount", - "fieldtype": "Float", + "fieldtype": "Currency", "in_list_view": 1, "label": "Allocated" }, @@ -176,7 +176,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-12-08 13:57:30.098239", + "modified": "2026-01-05 14:18:03.286224", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index a5e0b21a9af..0091d792f33 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -18,12 +18,12 @@ class PaymentEntryReference(Document): account_type: DF.Data | None advance_voucher_no: DF.DynamicLink | None advance_voucher_type: DF.Link | None - allocated_amount: DF.Float + allocated_amount: DF.Currency bill_no: DF.Data | None due_date: DF.Date | None exchange_gain_loss: DF.Currency exchange_rate: DF.Float - outstanding_amount: DF.Float + outstanding_amount: DF.Currency parent: DF.Data parentfield: DF.Data parenttype: DF.Data @@ -34,7 +34,7 @@ class PaymentEntryReference(Document): reconcile_effect_on: DF.Date | None reference_doctype: DF.Link reference_name: DF.DynamicLink - total_amount: DF.Float + total_amount: DF.Currency # end: auto-generated types @property diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index c1a28d12350..bdc1f929ec8 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -131,7 +131,6 @@ class PaymentLedgerEntry(Document): account_type == "Profit and Loss" and self.company == dimension.company and dimension.mandatory_for_pl - and not dimension.disabled ): if not self.get(dimension.fieldname): frappe.throw( @@ -144,7 +143,6 @@ class PaymentLedgerEntry(Document): account_type == "Balance Sheet" and self.company == dimension.company and dimension.mandatory_for_bs - and not dimension.disabled ): if not self.get(dimension.fieldname): frappe.throw( diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index a261bc6b0b8..f12491fdf15 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -50,12 +50,10 @@ class TestPaymentOrder(IntegrationTestCase): def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account): payment_order = frappe.get_doc( - dict( - doctype="Payment Order", - company="_Test Company", - payment_order_type=order_type, - company_bank_account=bank_account, - ) + doctype="Payment Order", + company="_Test Company", + payment_order_type=order_type, + company_bank_account=bank_account, ) doc = make_payment_order(ref_doc.name, payment_order) doc.save() diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 1de9ff936e5..7d21a4ba41a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -100,7 +100,10 @@ class PaymentRequest(Document): subscription_plans: DF.Table[SubscriptionPlanDetail] swift_number: DF.ReadOnly | None transaction_date: DF.Date | None + # end: auto-generated types + def on_discard(self): + self.db_set("status", "Cancelled") def validate(self): if self.get("__islocal"): diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js index f07d90b6ef3..57b05d19d83 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js @@ -13,9 +13,9 @@ frappe.ui.form.on("Period Closing Voucher", { return { filters: [ ["Account", "company", "=", frm.doc.company], - ["Account", "is_group", "=", "0"], + ["Account", "is_group", "=", 0], ["Account", "freeze_account", "=", "No"], - ["Account", "root_type", "in", "Liability, Equity"], + ["Account", "root_type", "in", ["Liability", "Equity"]], ], }; }); diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.json b/erpnext/accounts/doctype/pos_settings/pos_settings.json index a8413c599f3..ac0b884b3d7 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.json +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json @@ -36,9 +36,10 @@ "fieldtype": "Section Break" } ], + "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2025-06-06 11:36:44.885353", + "modified": "2026-01-02 18:18:17.586225", "modified_by": "Administrator", "module": "Accounts", "name": "POS Settings", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js index fda519e0851..7cce98f3323 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.js +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.js @@ -171,7 +171,7 @@ frappe.ui.form.on("Pricing Rule", { set_field_options("applicable_for", options.join("\n")); - if (!in_list(options, applicable_for)) applicable_for = null; + if (!options.includes(applicable_for)) applicable_for = null; frm.set_value("applicable_for", applicable_for); }, }); diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index b987d1579e8..8d35b7fcf85 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -48,13 +48,11 @@ class TestProcessDeferredAccounting(IntegrationTestCase): check_gl_entries(self, si.name, original_gle, "2023-07-01") process_deferred_accounting = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date="2023-07-01", - start_date="2023-05-01", - end_date="2023-06-30", - type="Income", - ) + doctype="Process Deferred Accounting", + posting_date="2023-07-01", + start_date="2023-05-01", + end_date="2023-06-30", + type="Income", ) process_deferred_accounting.insert() @@ -80,13 +78,11 @@ class TestProcessDeferredAccounting(IntegrationTestCase): def test_pda_submission_and_cancellation(self): pda = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date="2019-01-01", - start_date="2019-01-01", - end_date="2019-01-31", - type="Income", - ) + doctype="Process Deferred Accounting", + posting_date="2019-01-01", + start_date="2019-01-01", + end_date="2019-01-31", + type="Income", ) pda.submit() pda.cancel() diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index 95c6308ec9b..092081308c9 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -35,7 +35,10 @@ class ProcessPaymentReconciliation(Document): ] to_invoice_date: DF.Date | None to_payment_date: DF.Date | None + # end: auto-generated types + def on_discard(self): + self.db_set("status", "Cancelled") def validate(self): self.validate_receivable_payable_account() diff --git a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py index c63c5652cb3..c5435e847e4 100644 --- a/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py +++ b/erpnext/accounts/doctype/process_period_closing_voucher/process_period_closing_voucher.py @@ -36,7 +36,10 @@ class ProcessPeriodClosingVoucher(Document): parent_pcv: DF.Link status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"] z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail] + # end: auto-generated types + def on_discard(self): + self.db_set("status", "Cancelled") def validate(self): self.status = "Queued" diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js index 43261e4080a..920b9a99eac 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.js @@ -46,7 +46,7 @@ frappe.ui.form.on("Promotional Scheme", { set_field_options("applicable_for", options.join("\n")); - if (!in_list(options, applicable_for)) applicable_for = null; + if (!options.includes(applicable_for)) applicable_for = null; frm.set_value("applicable_for", applicable_for); }, diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 9392a0e3220..cb771bdc35a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1249,14 +1249,12 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin): pi.submit() pda1 = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Expense", - company="_Test Company", - ) + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Expense", + company="_Test Company", ) pda1.insert() @@ -1500,6 +1498,8 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin): def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1) + company = "_Test Company" tds_account_args = { diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index d5755cb3719..819ced1c911 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -115,6 +115,10 @@ class RepostAccountingLedger(Document): def generate_preview(self): from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns + if not self.vouchers: + frappe.msgprint(_("Add vouchers to generate preview.")) + return + gl_columns = [] gl_data = [] @@ -142,6 +146,7 @@ class RepostAccountingLedger(Document): account_repost_doc=self.name, is_async=True, job_name=job_name, + enqueue_after_commit=True, ) frappe.msgprint(_("Repost has started in the background")) else: diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json index 2ad0cf27625..ba8188c1440 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json @@ -14,10 +14,12 @@ "options": "Repost Allowed Types" } ], + "grid_page_length": 50, + "hide_toolbar": 1, "in_create": 1, "issingle": 1, "links": [], - "modified": "2024-06-06 13:56:37.908879", + "modified": "2026-01-02 18:19:08.888368", "modified_by": "Administrator", "module": "Accounts", "name": "Repost Accounting Ledger Settings", @@ -43,8 +45,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py index 637234fea1e..d6ef25cd1b7 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.py @@ -22,8 +22,8 @@ class RepostAccountingLedgerSettings(Document): from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes allowed_types: DF.Table[RepostAllowedTypes] - # end: auto-generated types + def validate(self): self.update_property_for_accounting_dimension() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1b98556f0c6..67dae79d083 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2510,14 +2510,12 @@ class TestSalesInvoice(ERPNextTestSuite): si.submit() pda1 = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company", - ) + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", ) pda1.insert() @@ -2568,14 +2566,12 @@ class TestSalesInvoice(ERPNextTestSuite): si.submit() pda1 = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date="2019-03-31", - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company", - ) + doctype="Process Deferred Accounting", + posting_date="2019-03-31", + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", ) pda1.insert() @@ -3478,14 +3474,12 @@ class TestSalesInvoice(ERPNextTestSuite): frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", getdate("2019-01-31")) pda1 = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company", - ) + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", ) pda1.insert() diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 50cff8c9e7c..77885cacac9 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -51,7 +51,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "\nTrialing\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", + "options": "\nTrialing\nActive\nGrace Period\nCancelled\nUnpaid\nCompleted", "read_only": 1 }, { @@ -267,7 +267,7 @@ "link_fieldname": "subscription" } ], - "modified": "2024-03-27 13:10:47.578120", + "modified": "2025-12-23 19:42:52.036034", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", @@ -311,8 +311,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index e2cd4127049..0b3da559e39 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -25,7 +25,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate -from erpnext.accounts.party import get_party_account_currency class InvoiceCancelled(frappe.ValidationError): @@ -77,7 +76,7 @@ class Subscription(Document): purchase_tax_template: DF.Link | None sales_tax_template: DF.Link | None start_date: DF.Date | None - status: DF.Literal["", "Trialing", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"] + status: DF.Literal["", "Trialing", "Active", "Grace Period", "Cancelled", "Unpaid", "Completed"] submit_invoice: DF.Check trial_period_end: DF.Date | None trial_period_start: DF.Date | None @@ -223,13 +222,17 @@ class Subscription(Document): """ if self.is_trialling(): self.status = "Trialing" - elif self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date): + elif ( + not self.has_outstanding_invoice() + and self.end_date + and getdate(posting_date) > getdate(self.end_date) + ): self.status = "Completed" elif self.is_past_grace_period(): self.status = self.get_status_for_past_grace_period() self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None elif self.current_invoice_is_past_due() and not self.is_past_grace_period(): - self.status = "Past Due Date" + self.status = "Grace Period" elif not self.has_outstanding_invoice(): self.status = "Active" @@ -432,7 +435,6 @@ class Subscription(Document): items_list = self.get_items_from_plans(self.plans, is_prorate()) for item in items_list: - item["cost_center"] = self.cost_center invoice.append("items", item) # Taxes @@ -564,6 +566,17 @@ class Subscription(Document): self.current_invoice_start, self.current_invoice_end ) and self.can_generate_new_invoice(posting_date): self.generate_invoice(posting_date=posting_date) + if self.end_date: + next_start = add_days(self.current_invoice_end, 1) + + if getdate(next_start) > getdate(self.end_date): + if self.cancel_at_period_end: + self.cancel_subscription() + else: + self.set_subscription_status(posting_date=posting_date) + + self.save() + return self.update_subscription_period(add_days(self.current_invoice_end, 1)) elif posting_date and getdate(posting_date) > getdate(self.current_invoice_end): self.update_subscription_period() diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 7bb72a1dcd7..2f1c1742d0b 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -17,6 +17,7 @@ from frappe.utils.data import ( nowdate, ) +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor EXTRA_TEST_RECORD_DEPENDENCIES = ("UOM", "Item Group", "Item") @@ -144,17 +145,17 @@ class TestSubscription(IntegrationTestCase): subscription = create_subscription(start_date=add_days(nowdate(), -1000)) subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice - self.assertEqual(subscription.status, "Past Due Date") + self.assertEqual(subscription.status, "Grace Period") subscription.process() - # Grace period is 1000 days so status should remain as Past Due Date - self.assertEqual(subscription.status, "Past Due Date") + # Grace period is 1000 days so status should remain as Grace Period + self.assertEqual(subscription.status, "Grace Period") subscription.process() - self.assertEqual(subscription.status, "Past Due Date") + self.assertEqual(subscription.status, "Grace Period") subscription.process() - self.assertEqual(subscription.status, "Past Due Date") + self.assertEqual(subscription.status, "Grace Period") settings.grace_period = grace_period settings.save() @@ -583,6 +584,105 @@ class TestSubscription(IntegrationTestCase): subscription.process(nowdate()) self.assertEqual(len(subscription.invoices), 1) + def test_subscription_auto_cancellation(self): + create_plan( + plan_name="_Test plan name 10", + cost=80, + currency="INR", + billing_interval="Day", + billing_interval_count=3, + ) + start_date = getdate("2025-01-01") + subscription = create_subscription( + start_date=start_date, + end_date=add_days(start_date, 8), + cancel_at_period_end=1, + generate_new_invoices_past_due_date=1, + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test plan name 10", "qty": 1}], + ) + subscription.process(posting_date=add_days(start_date, 2)) + self.assertEqual(len(subscription.invoices), 1) + + subscription.process(posting_date=add_days(start_date, 5)) + self.assertEqual(len(subscription.invoices), 2) + + subscription.process(posting_date=add_days(start_date, 8)) + self.assertEqual(len(subscription.invoices), 3) + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_auto_cancellation_uneven_cycle(self): + create_plan( + plan_name="_Test plan name 10", + cost=80, + currency="INR", + billing_interval="Day", + billing_interval_count=3, + ) + start_date = getdate("2025-01-01") + subscription = create_subscription( + start_date=start_date, + end_date=add_days(start_date, 6), + cancel_at_period_end=1, + generate_new_invoices_past_due_date=1, + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test plan name 10", "qty": 1}], + ) + + subscription.process(posting_date=add_days(start_date, 2)) + self.assertEqual(len(subscription.invoices), 1) + + subscription.process(posting_date=add_days(start_date, 5)) + self.assertEqual(len(subscription.invoices), 2) + + # partial last cycle invoice + subscription.process(posting_date=add_days(start_date, 6)) + self.assertEqual(len(subscription.invoices), 3) + + self.assertEqual(subscription.status, "Cancelled") + + self.assertRaises(frappe.ValidationError, subscription.process, posting_date=add_days(start_date, 7)) + + def test_subscription_auto_completion(self): + create_plan( + plan_name="_Test Plan 3 Day", + cost=100, + billing_interval="Day", + billing_interval_count=3, + currency="INR", + ) + + start_date = getdate("2025-01-01") + end_date = add_days(start_date, 6) + + subscription = create_subscription( + start_date=start_date, + end_date=end_date, + party_type="Customer", + party="_Test Customer", + generate_invoice_at="Beginning of the current subscription period", + generate_new_invoices_past_due_date=1, + plans=[{"plan": "_Test Plan 3 Day", "qty": 1}], + ) + + for day in range(0, 10): + if subscription.status == "Cancelled": + break + subscription.process(posting_date=add_days(start_date, day)) + + invoices = frappe.get_all( + "Sales Invoice", + filters={"subscription": subscription.name, "docstatus": 1}, + fields=["name", "from_date", "to_date"], + order_by="from_date asc", + ) + for invoice in invoices: + pi = get_payment_entry("Sales Invoice", invoice.name) + pi.submit() + # After processing through all days, subscription should be completed + subscription.process(posting_date=add_days(end_date, 1)) + self.assertEqual(subscription.status, "Completed") + def make_plans(): create_plan(plan_name="_Test Plan Name", cost=900, currency="INR") @@ -653,12 +753,13 @@ def reset_settings(): def create_subscription(**kwargs): subscription = frappe.new_doc("Subscription") - subscription.party_type = (kwargs.get("party_type") or "Customer",) + subscription.party_type = kwargs.get("party_type") or "Customer" subscription.company = kwargs.get("company") or "_Test Company" subscription.party = kwargs.get("party") or "_Test Customer" subscription.trial_period_start = kwargs.get("trial_period_start") subscription.trial_period_end = kwargs.get("trial_period_end") subscription.start_date = kwargs.get("start_date") + subscription.end_date = kwargs.get("end_date") subscription.generate_invoice_at = kwargs.get("generate_invoice_at") subscription.additional_discount_percentage = kwargs.get("additional_discount_percentage") subscription.additional_discount_amount = kwargs.get("additional_discount_amount") @@ -667,6 +768,7 @@ def create_subscription(**kwargs): subscription.submit_invoice = kwargs.get("submit_invoice") subscription.days_until_due = kwargs.get("days_until_due") subscription.number_of_days = kwargs.get("number_of_days") + subscription.cancel_at_period_end = kwargs.get("cancel_at_period_end") if not kwargs.get("plans"): subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json index 827b0feb259..326fee345dc 100644 --- a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json +++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json @@ -30,9 +30,11 @@ "label": "Prorate" } ], + "grid_page_length": 50, + "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:10:48.283833", + "modified": "2026-01-02 18:18:34.671062", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Settings", @@ -70,8 +72,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 38561815d65..782f5a06cfb 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -1214,8 +1214,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): # First invoice - below threshold, should be under withheld pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True) pi.apply_tds = 1 - pi.tax_withholding_category = "Test Multi Invoice Category" - pi.save() pi.submit() invoices.append(pi) @@ -1478,7 +1476,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=12000, do_not_save=True) pi.apply_tds = 1 - pi.tax_withholding_category = "Test Multi Invoice Category" advances = pi.get_advance_entries() pi.append( "advances", @@ -2096,7 +2093,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): # Create purchase invoice that settles the payment entry pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=8000, do_not_save=True) pi.apply_tds = 1 - pi.tax_withholding_category = "Test Multi Invoice Category" advances = pi.get_advance_entries() pi.append( "advances", @@ -2719,7 +2715,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): # Create purchase invoice with manual override pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=20000, do_not_save=True) pi.apply_tds = 1 - pi.tax_withholding_category = "Test Multi Invoice Category" pi.ignore_tax_withholding_threshold = 1 pi.save() @@ -2749,7 +2744,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): # Step 2: Create Purchase Invoice with partial adjustment and manual rate change pi = create_purchase_invoice(supplier="Test TDS Supplier8", rate=80000, do_not_save=True) - pi.tax_withholding_category = "Test Multi Invoice Category" pi.override_tax_withholding_entries = 1 # Enable manual override pi.tax_withholding_entries = [] @@ -2790,6 +2784,7 @@ class TestTaxWithholdingCategory(IntegrationTestCase): ) pi.save() + pi.reload() pi.submit() # Step 3: Verify the tax withholding entries @@ -2870,7 +2865,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): # Step 2: Create Purchase Invoice with partial adjustment and manual rate change pi = create_purchase_invoice(supplier="Test TDS Supplier8", rate=80000, do_not_save=True) - pi.tax_withholding_category = "Test Multi Invoice Category" pi.override_tax_withholding_entries = 1 # Enable manual override pi.tax_withholding_entries = [] @@ -2911,6 +2905,7 @@ class TestTaxWithholdingCategory(IntegrationTestCase): ) pi.save() + pi.reload() pi.submit() # Step 3: Verify the tax withholding entries @@ -2993,7 +2988,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase): self.validate_tax_withholding_entries("Payment Entry", pe.name, pe_expected) pi = create_purchase_invoice(supplier="Test TDS Supplier8", rate=50000, do_not_save=True) - pi.tax_withholding_category = "Test Multi Invoice Category" pi.override_tax_withholding_entries = 1 pi.tax_withholding_entries = [] diff --git a/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py b/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py index 96a2768e68c..4abbd2c28e4 100644 --- a/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py +++ b/erpnext/accounts/doctype/tax_withholding_entry/tax_withholding_entry.py @@ -377,30 +377,23 @@ class TaxWithholdingController: return category_names def calculate(self): - # Always get category details first for account mapping self.category_details = self._get_category_details() + self._update_taxable_amounts() + if not self.doc.override_tax_withholding_entries: self._generate_withholding_entries() - # Final processing - entry status and tax_update self._process_withholding_entries() def _generate_withholding_entries(self): - # Clear existing entries self.doc.tax_withholding_entries = [] - # Calculate taxable amounts for each category - self._update_taxable_amounts() - - # Apply threshold rules self._evaluate_thresholds() - # Generate entries for each category for category in self.category_details.values(): self.entries += self._create_entries_for_category(category) - # Add all generated entries to the document self.doc.extend("tax_withholding_entries", self.entries) def _create_entries_for_category(self, category): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 47323ad0f40..7c5dcedaf74 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -14,6 +14,7 @@ from frappe.utils.dashboard import cache_source import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, + get_checks_for_pl_and_bs_accounts, ) from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( get_dimension_filter_map, @@ -153,7 +154,7 @@ def validate_disabled_accounts(gl_map): def validate_accounting_period(gl_map): accounting_periods = frappe.db.sql( """ SELECT - ap.name as name + ap.name as name, ap.exempted_role as exempted_role FROM `tabAccounting Period` ap, `tabClosed Document` cd WHERE @@ -173,6 +174,10 @@ def validate_accounting_period(gl_map): ) if accounting_periods: + if accounting_periods[0].exempted_role: + exempted_roles = accounting_periods[0].exempted_role + if exempted_roles in frappe.get_roles(): + return frappe.throw( _( "You cannot create or cancel any accounting entries with in the closed Accounting Period {0}" @@ -624,6 +629,18 @@ def update_accounting_dimensions(round_off_gle): for dimension in dimensions: round_off_gle[dimension] = dimension_values.get(dimension) + else: + report_type = frappe.get_cached_value("Account", round_off_gle.account, "report_type") + for dimension in get_checks_for_pl_and_bs_accounts(): + if ( + round_off_gle.company == dimension.company + and ( + (report_type == "Profit and Loss" and dimension.mandatory_for_pl) + or (report_type == "Balance Sheet" and dimension.mandatory_for_bs) + ) + and dimension.default_dimension + ): + round_off_gle[dimension.fieldname] = dimension.default_dimension def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False): diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index c2af854f9cc..61a4a976a19 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -1063,3 +1063,21 @@ def add_party_account(party_type, party, company, account): def render_address(address, check_permissions=True): return frappe.call(_render_address, address, check_permissions=check_permissions) + + +def validate_party_currency_before_merging(party_type, old_party, new_party): + for company in frappe.get_all("Company"): + old_party_currency = get_party_gle_currency(party_type, old_party, company.name) + new_party_currency = get_party_gle_currency(party_type, new_party, company.name) + + if old_party_currency and new_party_currency and old_party_currency != new_party_currency: + frappe.throw( + _( + "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." + ).format( + party_type, + old_party, + new_party, + company.name, + ) + ) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 0562b0da86f..4d74a5c2d7c 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -165,6 +165,10 @@ frappe.query_reports["Accounts Payable"] = { var filters = report.get_values(); frappe.set_route("query-report", "Accounts Payable Summary", { company: filters.company }); }); + + if (frappe.boot.sysdefaults.default_ageing_range) { + report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range); + } }, }; diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 6a043f5e185..a4cb0584bf1 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -114,6 +114,10 @@ frappe.query_reports["Accounts Payable Summary"] = { var filters = report.get_values(); frappe.set_route("query-report", "Accounts Payable", { company: filters.company }); }); + + if (frappe.boot.sysdefaults.default_ageing_range) { + report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range); + } }, }; diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index ddee9c6b6b0..a05b17bfade 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -192,6 +192,10 @@ frappe.query_reports["Accounts Receivable"] = { var filters = report.get_values(); frappe.set_route("query-report", "Accounts Receivable Summary", { company: filters.company }); }); + + if (frappe.boot.sysdefaults.default_ageing_range) { + report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range); + } }, }; diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 1ac2b27ca71..c8e59d6e054 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -137,6 +137,10 @@ frappe.query_reports["Accounts Receivable Summary"] = { var filters = report.get_values(); frappe.set_route("query-report", "Accounts Receivable", { company: filters.company }); }); + + if (frappe.boot.sysdefaults.default_ageing_range) { + report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range); + } }, }; diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.json b/erpnext/accounts/report/budget_variance_report/budget_variance_report.json index 8e49bc532d3..20593612f84 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.json +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.json @@ -1,35 +1,40 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-06-18 12:56:36", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:19:06.964033", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Budget Variance Report", - "owner": "Administrator", - "ref_doctype": "Cost Center", - "report_name": "Budget Variance Report", - "report_type": "Script Report", + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2013-06-18 12:56:36", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "letter_head": null, + "modified": "2025-12-30 14:51:02.061226", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Budget Variance Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Cost Center", + "report_name": "Budget Variance Report", + "report_type": "Script Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Auditor" - }, + }, { "role": "Accounts User" - }, + }, { "role": "Sales User" - }, + }, { "role": "Purchase User" } - ] -} \ No newline at end of file + ], + "timeout": 0 +} diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index db42d23a839..a53cdcc1b40 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -1,14 +1,12 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import datetime +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt import frappe from frappe import _ -from frappe.utils import flt, formatdate +from frappe.utils import add_months, flt, formatdate -from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges +from erpnext.accounts.utils import get_fiscal_year +from erpnext.controllers.trends import get_period_date_ranges def execute(filters=None): @@ -19,57 +17,282 @@ def execute(filters=None): if filters.get("budget_against_filter"): dimensions = filters.get("budget_against_filter") else: - dimensions = get_cost_centers(filters) + dimensions = get_budget_dimensions(filters) - period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"]) - cam_map = get_dimension_account_month_map(filters) + budget_records = get_budget_records(filters, dimensions) + budget_map = build_budget_map(budget_records, filters) + data = build_report_data(budget_map, filters) + + chart_data = build_comparison_chart_data(filters, columns, data) + + return columns, data, None, chart_data + + +def get_budget_records(filters, dimensions): + budget_against_field = frappe.scrub(filters["budget_against"]) + + return frappe.db.sql( + f""" + SELECT + b.name, + b.account, + b.{budget_against_field} AS dimension, + b.budget_amount, + b.from_fiscal_year, + b.to_fiscal_year, + b.budget_start_date, + b.budget_end_date + FROM + `tabBudget` b + WHERE + b.company = %s + AND b.docstatus = 1 + AND b.budget_against = %s + AND b.{budget_against_field} IN ({', '.join(['%s'] * len(dimensions))}) + AND ( + b.from_fiscal_year <= %s + AND b.to_fiscal_year >= %s + ) + """, + ( + filters.company, + filters.budget_against, + *dimensions, + filters.to_fiscal_year, + filters.from_fiscal_year, + ), + as_dict=True, + ) + + +def build_budget_map(budget_records, filters): + """ + Builds a nested dictionary structure aggregating budget and actual amounts. + + Structure: {dimension_name: {account_name: {fiscal_year: {month_name: {"budget": amount, "actual": amount}}}}} + """ + budget_map = {} + + for budget in budget_records: + actual_amt = get_actual_transactions(budget.dimension, filters) + budget_map.setdefault(budget.dimension, {}) + budget_map[budget.dimension].setdefault(budget.account, {}) + + budget_distributions = get_budget_distributions(budget) + + for row in budget_distributions: + months = get_months_in_range(row.start_date, row.end_date) + monthly_budget = flt(row.amount) / len(months) + + for month_date in months: + fiscal_year = get_fiscal_year(month_date)[0] + month = month_date.strftime("%B") + + budget_map[budget.dimension][budget.account].setdefault(fiscal_year, {}) + budget_map[budget.dimension][budget.account][fiscal_year].setdefault( + month, + { + "budget": 0, + "actual": 0, + }, + ) + + budget_map[budget.dimension][budget.account][fiscal_year][month]["budget"] += monthly_budget + + for ad in actual_amt.get(budget.account, []): + if ad.month_name == month and ad.fiscal_year == fiscal_year: + budget_map[budget.dimension][budget.account][fiscal_year][month]["actual"] += flt( + ad.debit + ) - flt(ad.credit) + + return budget_map + + +def get_actual_transactions(dimension_name, filters): + budget_against = frappe.scrub(filters.get("budget_against")) + cost_center_filter = "" + + if filters.get("budget_against") == "Cost Center" and dimension_name: + cc_lft, cc_rgt = frappe.db.get_value("Cost Center", dimension_name, ["lft", "rgt"]) + cost_center_filter = f""" + and lft >= "{cc_lft}" + and rgt <= "{cc_rgt}" + """ + + actual_transactions = frappe.db.sql( + f""" + select + gl.account, + gl.debit, + gl.credit, + gl.fiscal_year, + MONTHNAME(gl.posting_date) as month_name, + b.{budget_against} as budget_against + from + `tabGL Entry` gl, + `tabBudget` b + where + b.docstatus = 1 + and b.account=gl.account + and b.{budget_against} = gl.{budget_against} + and gl.fiscal_year between %s and %s + and gl.is_cancelled = 0 + and b.{budget_against} = %s + and exists( + select + name + from + `tab{filters.budget_against}` + where + name = gl.{budget_against} + {cost_center_filter} + ) + group by + gl.name + order by gl.fiscal_year + """, + (filters.from_fiscal_year, filters.to_fiscal_year, dimension_name), + as_dict=1, + ) + + actual_transactions_map = {} + for transaction in actual_transactions: + actual_transactions_map.setdefault(transaction.account, []).append(transaction) + + return actual_transactions_map + + +def get_budget_distributions(budget): + return frappe.db.sql( + """ + SELECT start_date, end_date, amount, percent + FROM `tabBudget Distribution` + WHERE parent = %s + ORDER BY start_date ASC + """, + (budget.name,), + as_dict=True, + ) + + +def get_months_in_range(start_date, end_date): + months = [] + current = start_date + + while current <= end_date: + months.append(current) + current = add_months(current, 1) + + return months + + +def build_report_data(budget_map, filters): data = [] - for dimension in dimensions: - dimension_items = cam_map.get(dimension) - if dimension_items: - data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0) - chart = get_chart_data(filters, columns, data) + show_cumulative = filters.get("show_cumulative") and filters.get("period") != "Yearly" + periods = get_periods(filters) - return columns, data, None, chart + for dimension, accounts in budget_map.items(): + for account, fiscal_year_map in accounts.items(): + row = { + "budget_against": dimension, + "account": account, + } + running_budget = 0 + running_actual = 0 + total_budget = 0 + total_actual = 0 -def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): - for account, monthwise_data in dimension_items.items(): - row = [dimension, account] - totals = [0, 0, 0] - for year in get_fiscal_years(filters): - last_total = 0 - for relevant_months in period_month_ranges: - period_data = [0, 0, 0] - for month in relevant_months: - if monthwise_data.get(year[0]): - month_data = monthwise_data.get(year[0]).get(month, {}) - for i, fieldname in enumerate(["target", "actual", "variance"]): - value = flt(month_data.get(fieldname)) - period_data[i] += value - totals[i] += value + for period in periods: + fiscal_year = period["fiscal_year"] + months = get_months_between(period["from_date"], period["to_date"]) - period_data[0] += last_total + period_budget = 0 + period_actual = 0 - if DCC_allocation: - period_data[0] = period_data[0] * (DCC_allocation / 100) - period_data[1] = period_data[1] * (DCC_allocation / 100) + month_map = fiscal_year_map.get(fiscal_year, {}) - if filters.get("show_cumulative"): - last_total = period_data[0] - period_data[1] + for month in months: + values = month_map.get(month) + if values: + period_budget += values.get("budget", 0) + period_actual += values.get("actual", 0) - period_data[2] = period_data[0] - period_data[1] - row += period_data - totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly": - row += totals - data.append(row) + if show_cumulative: + running_budget += period_budget + running_actual += period_actual + display_budget = running_budget + display_actual = running_actual + else: + display_budget = period_budget + display_actual = period_actual + + total_budget += period_budget + total_actual += period_actual + + if filters["period"] == "Yearly": + budget_label = _("Budget") + " " + fiscal_year + actual_label = _("Actual") + " " + fiscal_year + variance_label = _("Variance") + " " + fiscal_year + else: + budget_label = _("Budget") + f" ({period['label_suffix']}) {fiscal_year}" + actual_label = _("Actual") + f" ({period['label_suffix']}) {fiscal_year}" + variance_label = _("Variance") + f" ({period['label_suffix']}) {fiscal_year}" + + row[frappe.scrub(budget_label)] = display_budget + row[frappe.scrub(actual_label)] = display_actual + row[frappe.scrub(variance_label)] = display_budget - display_actual + + if filters["period"] != "Yearly": + row["total_budget"] = total_budget + row["total_actual"] = total_actual + row["total_variance"] = total_budget - total_actual + + data.append(row) return data +def get_periods(filters): + periods = [] + + group_months = filters["period"] != "Monthly" + + for (fiscal_year,) in get_fiscal_years(filters): + for from_date, to_date in get_period_date_ranges(filters["period"], fiscal_year): + if filters["period"] == "Yearly": + label_suffix = fiscal_year + else: + if group_months: + label_suffix = formatdate(from_date, "MMM") + "-" + formatdate(to_date, "MMM") + else: + label_suffix = formatdate(from_date, "MMM") + + periods.append( + { + "fiscal_year": fiscal_year, + "from_date": from_date, + "to_date": to_date, + "label_suffix": label_suffix, + } + ) + + return periods + + +def get_months_between(from_date, to_date): + months = [] + current = from_date + + while current <= to_date: + months.append(formatdate(current, "MMMM")) + current = add_months(current, 1) + + return months + + def get_columns(filters): columns = [ { @@ -81,7 +304,7 @@ def get_columns(filters): }, { "label": _("Account"), - "fieldname": "Account", + "fieldname": "account", "fieldtype": "Link", "options": "Account", "width": 150, @@ -134,7 +357,23 @@ def get_columns(filters): return columns -def get_cost_centers(filters): +def get_fiscal_years(filters): + fiscal_year = frappe.db.sql( + """ + select + name + from + `tabFiscal Year` + where + name between %(from_fiscal_year)s and %(to_fiscal_year)s + """, + {"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]}, + ) + + return fiscal_year + + +def get_budget_dimensions(filters): order_by = "" if filters.get("budget_against") == "Cost Center": order_by = "order by lft" @@ -163,222 +402,56 @@ def get_cost_centers(filters): ) # nosec -# Get dimension & target details -def get_dimension_target_details(filters): - budget_against = frappe.scrub(filters.get("budget_against")) - cond = "" - if filters.get("budget_against_filter"): - cond += f""" and b.{budget_against} in (%s)""" % ", ".join( - ["%s"] * len(filters.get("budget_against_filter")) - ) - - return frappe.db.sql( - f""" - select - b.{budget_against} as budget_against, - b.monthly_distribution, - ba.account, - ba.budget_amount, - b.fiscal_year - from - `tabBudget` b, - `tabBudget Account` ba - where - b.name = ba.parent - and b.docstatus = 1 - and b.fiscal_year between %s and %s - and b.budget_against = %s - and b.company = %s - {cond} - order by - b.fiscal_year - """, - tuple( - [ - filters.from_fiscal_year, - filters.to_fiscal_year, - filters.budget_against, - filters.company, - ] - + (filters.get("budget_against_filter") or []) - ), - as_dict=True, - ) - - -# Get target distribution details of accounts of cost center -def get_target_distribution_details(filters): - target_details = {} - for d in frappe.db.sql( - """ - select - md.name, - mdp.month, - mdp.percentage_allocation - from - `tabMonthly Distribution Percentage` mdp, - `tabMonthly Distribution` md - where - mdp.parent = md.name - and md.fiscal_year between %s and %s - order by - md.fiscal_year - """, - (filters.from_fiscal_year, filters.to_fiscal_year), - as_dict=1, - ): - target_details.setdefault(d.name, {}).setdefault(d.month, flt(d.percentage_allocation)) - - return target_details - - -# Get actual details from gl entry -def get_actual_details(name, filters): - budget_against = frappe.scrub(filters.get("budget_against")) - cond = "" - - if filters.get("budget_against") == "Cost Center": - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", name, ["lft", "rgt"]) - cond = f""" - and lft >= "{cc_lft}" - and rgt <= "{cc_rgt}" - """ - - ac_details = frappe.db.sql( - f""" - select - gl.account, - gl.debit, - gl.credit, - gl.fiscal_year, - MONTHNAME(gl.posting_date) as month_name, - b.{budget_against} as budget_against - from - `tabGL Entry` gl, - `tabBudget Account` ba, - `tabBudget` b - where - b.name = ba.parent - and b.docstatus = 1 - and ba.account=gl.account - and b.{budget_against} = gl.{budget_against} - and gl.fiscal_year between %s and %s - and gl.is_cancelled = 0 - and b.{budget_against} = %s - and exists( - select - name - from - `tab{filters.budget_against}` - where - name = gl.{budget_against} - {cond} - ) - group by - gl.name - order by gl.fiscal_year - """, - (filters.from_fiscal_year, filters.to_fiscal_year, name), - as_dict=1, - ) - - cc_actual_details = {} - for d in ac_details: - cc_actual_details.setdefault(d.account, []).append(d) - - return cc_actual_details - - -def get_dimension_account_month_map(filters): - dimension_target_details = get_dimension_target_details(filters) - tdd = get_target_distribution_details(filters) - - cam_map = {} - - for ccd in dimension_target_details: - actual_details = get_actual_details(ccd.budget_against, filters) - - for month_id in range(1, 13): - month = datetime.date(2013, month_id, 1).strftime("%B") - cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, {}).setdefault( - ccd.fiscal_year, {} - ).setdefault(month, frappe._dict({"target": 0.0, "actual": 0.0})) - - tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] - month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) - if ccd.monthly_distribution - else 100.0 / 12 - ) - - tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 - - for ad in actual_details.get(ccd.account, []): - if ad.month_name == month and ad.fiscal_year == ccd.fiscal_year: - tav_dict.actual += flt(ad.debit) - flt(ad.credit) - - return cam_map - - -def get_fiscal_years(filters): - fiscal_year = frappe.db.sql( - """ - select - name - from - `tabFiscal Year` - where - name between %(from_fiscal_year)s and %(to_fiscal_year)s - """, - {"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]}, - ) - - return fiscal_year - - -def get_chart_data(filters, columns, data): +def build_comparison_chart_data(filters, columns, data): if not data: return None - labels = [] + budget_fields = [] + actual_fields = [] - fiscal_year = get_fiscal_years(filters) - group_months = False if filters["period"] == "Monthly" else True + for col in columns: + fieldname = col.get("fieldname") + if not fieldname: + continue - for year in fiscal_year: - for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): - if filters["period"] == "Yearly": - labels.append(year[0]) - else: - if group_months: - label = ( - formatdate(from_date, format_string="MMM") - + "-" - + formatdate(to_date, format_string="MMM") - ) - labels.append(label) - else: - label = formatdate(from_date, format_string="MMM") - labels.append(label) + if fieldname.startswith("budget_"): + budget_fields.append(fieldname) + elif fieldname.startswith("actual_"): + actual_fields.append(fieldname) - no_of_columns = len(labels) + if not budget_fields or not actual_fields: + return None - budget_values, actual_values = [0] * no_of_columns, [0] * no_of_columns - for d in data: - values = d[2:] - index = 0 + labels = [ + col["label"].replace("Budget", "").strip() + for col in columns + if col.get("fieldname", "").startswith("budget_") + ] - for i in range(no_of_columns): - budget_values[i] += values[index] - actual_values[i] += values[index + 1] - index += 3 + budget_values = [0] * len(budget_fields) + actual_values = [0] * len(actual_fields) + + for row in data: + for i, field in enumerate(budget_fields): + budget_values[i] += flt(row.get(field)) + + for i, field in enumerate(actual_fields): + actual_values[i] += flt(row.get(field)) return { "data": { "labels": labels, "datasets": [ - {"name": _("Budget"), "chartType": "bar", "values": budget_values}, - {"name": _("Actual Expense"), "chartType": "bar", "values": actual_values}, + { + "name": _("Budget"), + "chartType": "bar", + "values": budget_values, + }, + { + "name": _("Actual Expense"), + "chartType": "bar", + "values": actual_values, + }, ], }, "type": "bar", diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index bf3f636bf7c..cf196f13037 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -44,3 +44,5 @@ frappe.query_reports[CF_REPORT_NAME]["filters"].push( fieldtype: "Check", } ); + +frappe.query_reports[CF_REPORT_NAME]["export_hidden_cols"] = true; diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index d918a936091..fcd9075a30f 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -101,14 +101,12 @@ class TestDeferredRevenueAndExpense(IntegrationTestCase, AccountsTestMixin): si.submit() pda = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date=nowdate(), - start_date="2021-05-01", - end_date="2021-08-01", - type="Income", - company=self.company, - ) + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2021-05-01", + end_date="2021-08-01", + type="Income", + company=self.company, ) pda.insert() pda.submit() @@ -173,14 +171,12 @@ class TestDeferredRevenueAndExpense(IntegrationTestCase, AccountsTestMixin): pi.submit() pda = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date=nowdate(), - start_date="2021-05-01", - end_date="2021-08-01", - type="Expense", - company=self.company, - ) + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2021-05-01", + end_date="2021-08-01", + type="Expense", + company=self.company, ) pda.insert() pda.submit() @@ -240,14 +236,12 @@ class TestDeferredRevenueAndExpense(IntegrationTestCase, AccountsTestMixin): si.submit() pda = frappe.get_doc( - dict( - doctype="Process Deferred Accounting", - posting_date=nowdate(), - start_date="2021-05-01", - end_date="2021-08-01", - type="Income", - company=self.company, - ) + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2021-05-01", + end_date="2021-08-01", + type="Income", + company=self.company, ) pda.insert() pda.submit() diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index f78775ec2ad..9c13b12ec40 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -13,7 +13,7 @@ } .financial-statements-blank-row td { - height: 37px; + height: 20px; } @@ -25,30 +25,37 @@ {% endif %}
| + | {%= report_columns[0].label %} | + {% for (let i=1, l=report_columns.length; i
|---|
{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}
diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js index 62482ac162c..ecddd7271ea 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.js @@ -81,5 +81,11 @@ frappe.query_reports["Trial Balance for Party"] = { label: __("Show zero values"), fieldtype: "Check", }, + { + fieldname: "exclude_zero_balance_parties", + label: __("Exclude Zero Balance Parties"), + fieldtype: "Check", + default: 1, + }, ], }; diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index 95484bb190b..b4bae5c1376 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -75,20 +75,20 @@ def get_data(filters, show_party_name): closing_debit, closing_credit = toggle_debit_credit(opening_debit + debit, opening_credit + credit) row.update({"closing_debit": closing_debit, "closing_credit": closing_credit}) - # totals - for col in total_row: - total_row[col] += row.get(col) - row.update({"currency": company_currency}) has_value = False if opening_debit or opening_credit or debit or credit or closing_debit or closing_credit: has_value = True + # Exclude zero balance parties if filter is set + if filters.get("exclude_zero_balance_parties") and not closing_debit and not closing_credit: + continue if cint(filters.show_zero_values) or has_value: data.append(row) - - # Add total row + # totals + for col in total_row: + total_row[col] += row.get(col) total_row.update({"party": "'" + _("Totals") + "'", "currency": company_currency}) data.append(total_row) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 557eb21441b..7dfe9041506 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1146,7 +1146,7 @@ def get_company_default(company, fieldname, ignore_validation=False): if not ignore_validation and not value: throw( _("Please set default {0} in Company {1}").format( - frappe.get_meta("Company").get_label(fieldname), company + _(frappe.get_meta("Company").get_label(fieldname)), company ) ) diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index 07c3e472944..8205c258068 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -6,7 +6,7 @@ "label": "Profit and Loss" } ], - "content": "[{\"id\":\"nDhfcJYbKH\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Payment\",\"col\":3}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Tax Masters\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Banking\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}}]", + "content": "[{\"id\":\"nDhfcJYbKH\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Payment\",\"col\":3}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Tax Masters\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Banking\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}}]", "creation": "2020-03-02 15:41:59.515192", "custom_blocks": [], "docstatus": 0, @@ -14,7 +14,7 @@ "for_user": "", "hide_custom": 0, "icon": "accounting", - "idx": 0, + "idx": 3, "indicator_color": "", "is_hidden": 0, "label": "Accounting", @@ -587,25 +587,25 @@ "type": "Link" } ], - "modified": "2025-11-17 14:35:00.910131", + "modified": "2025-12-24 13:20:34.857205", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", "number_cards": [ { - "label": "Total Outgoing Bills", + "label": "Outgoing Bills", "number_card_name": "Total Outgoing Bills" }, { - "label": "Total Incoming Bills", + "label": "Incoming Bills", "number_card_name": "Total Incoming Bills" }, { - "label": "Total Incoming Payment", + "label": "Incoming Payment", "number_card_name": "Total Incoming Payment" }, { - "label": "Total Outgoing Payment", + "label": "Outgoing Payment", "number_card_name": "Total Outgoing Payment" } ], diff --git a/erpnext/accounts/workspace/financial_reports/financial_reports.json b/erpnext/accounts/workspace/financial_reports/financial_reports.json index 3ab4a3e22d7..3211845f5ea 100644 --- a/erpnext/accounts/workspace/financial_reports/financial_reports.json +++ b/erpnext/accounts/workspace/financial_reports/financial_reports.json @@ -1,13 +1,19 @@ { - "charts": [], - "content": "[{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Ledgers\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", + "app": "erpnext", + "charts": [ + { + "chart_name": "Profit and Loss", + "label": "Profit and Loss" + } + ], + "content": "[{\"id\":\"tS7ZWzC24I\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"8Ej2KxPxOt\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Ledgers\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2024-01-05 16:09:16.766939", "custom_blocks": [], "docstatus": 0, "doctype": "Workspace", "for_user": "", "hide_custom": 0, - "icon": "file", + "icon": "table", "idx": 0, "indicator_color": "", "is_hidden": 0, @@ -260,7 +266,7 @@ "type": "Link" } ], - "modified": "2024-01-18 22:13:07.596844", + "modified": "2025-12-24 12:49:25.266357", "modified_by": "Administrator", "module": "Accounts", "name": "Financial Reports", @@ -273,5 +279,6 @@ "roles": [], "sequence_id": 5.0, "shortcuts": [], - "title": "Financial Reports" -} \ No newline at end of file + "title": "Financial Reports", + "type": "Workspace" +} diff --git a/erpnext/accounts/workspace/payables/payables.json b/erpnext/accounts/workspace/payables/payables.json deleted file mode 100644 index 96c626c7291..00000000000 --- a/erpnext/accounts/workspace/payables/payables.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "charts": [], - "content": "[{\"id\":\"rMMsfn2eB4\",\"type\":\"header\",\"data\":{\"text\":\"Shortcuts\",\"col\":12}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Payable\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jAcOH-cC-Q\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"7dj93PEUjW\",\"type\":\"card\",\"data\":{\"card_name\":\"Invoicing\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"9yseIkdG50\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", - "creation": "2024-01-05 15:29:11.144373", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "arrow-left", - "idx": 0, - "indicator_color": "", - "is_hidden": 0, - "label": "Payables", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Invoicing", - "link_count": 2, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Purchase Invoice", - "link_count": 0, - "link_to": "Purchase Invoice", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Supplier", - "link_count": 0, - "link_to": "Supplier", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payments", - "link_count": 3, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Payment Entry", - "link_count": 0, - "link_to": "Payment Entry", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Journal Entry", - "link_count": 0, - "link_to": "Journal Entry", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payment Reconciliation", - "link_count": 0, - "link_to": "Payment Reconciliation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "link_count": 7, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Accounts Payable", - "link_count": 0, - "link_to": "Accounts Payable", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Accounts Payable Summary", - "link_count": 0, - "link_to": "Accounts Payable Summary", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Purchase Register", - "link_count": 0, - "link_to": "Purchase Register", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Item-wise Purchase Register", - "link_count": 0, - "link_to": "Item-wise Purchase Register", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Purchase Order Analysis", - "link_count": 0, - "link_to": "Purchase Order Analysis", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Received Items To Be Billed", - "link_count": 0, - "link_to": "Received Items To Be Billed", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 1, - "label": "Supplier Ledger Summary", - "link_count": 0, - "link_to": "Supplier Ledger Summary", - "link_type": "Report", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2024-01-18 22:09:46.221549", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payables", - "number_cards": [], - "owner": "Administrator", - "parent_page": "Accounting", - "public": 1, - "quick_lists": [], - "restrict_to_domain": "", - "roles": [], - "sequence_id": 3.0, - "shortcuts": [ - { - "doc_view": "", - "label": "Accounts Payable", - "link_to": "Accounts Payable", - "type": "Report" - }, - { - "doc_view": "", - "label": "Purchase Invoice", - "link_to": "Purchase Invoice", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Journal Entry", - "link_to": "Journal Entry", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Payment Entry", - "link_to": "Payment Entry", - "type": "DocType" - } - ], - "title": "Payables" -} \ No newline at end of file diff --git a/erpnext/accounts/workspace/receivables/receivables.json b/erpnext/accounts/workspace/receivables/receivables.json deleted file mode 100644 index 6fa8c099a46..00000000000 --- a/erpnext/accounts/workspace/receivables/receivables.json +++ /dev/null @@ -1,254 +0,0 @@ -{ - "charts": [], - "content": "[{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"Shortcuts\",\"col\":12}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"5yHldR0JNk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"POS Invoice\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"ILlIxJuexy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Cost Center\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"jLgv00c6ek\",\"type\":\"card\",\"data\":{\"card_name\":\"Invoicing\",\"col\":4}},{\"id\":\"npwfXlz0u1\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"am70C27Jrb\",\"type\":\"card\",\"data\":{\"card_name\":\"Dunning\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", - "creation": "2024-01-05 15:29:21.084241", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "arrow-right", - "idx": 0, - "indicator_color": "", - "is_hidden": 0, - "label": "Receivables", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Invoicing", - "link_count": 2, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Sales Invoice", - "link_count": 0, - "link_to": "Sales Invoice", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Customer", - "link_count": 0, - "link_to": "Customer", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payments", - "link_count": 4, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payment Entry", - "link_count": 0, - "link_to": "Payment Entry", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payment Request", - "link_count": 0, - "link_to": "Payment Request", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payment Reconciliation", - "link_count": 0, - "link_to": "Payment Reconciliation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payment Gateway Account", - "link_count": 0, - "link_to": "Payment Gateway Account", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Dunning", - "link_count": 2, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Dunning", - "link_count": 0, - "link_to": "Dunning", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Dunning Type", - "link_count": 0, - "link_to": "Dunning Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Reports", - "link_count": 6, - "link_type": "DocType", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "Sales Invoice", - "hidden": 0, - "is_query_report": 1, - "label": "Accounts Receivable", - "link_count": 0, - "link_to": "Accounts Receivable", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Sales Invoice", - "hidden": 0, - "is_query_report": 1, - "label": "Accounts Receivable Summary", - "link_count": 0, - "link_to": "Accounts Receivable Summary", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Sales Invoice", - "hidden": 0, - "is_query_report": 1, - "label": "Sales Register", - "link_count": 0, - "link_to": "Sales Register", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Sales Invoice", - "hidden": 0, - "is_query_report": 1, - "label": "Item-wise Sales Register", - "link_count": 0, - "link_to": "Item-wise Sales Register", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Sales Invoice", - "hidden": 0, - "is_query_report": 1, - "label": "Sales Order Analysis", - "link_count": 0, - "link_to": "Sales Order Analysis", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Sales Invoice", - "hidden": 0, - "is_query_report": 1, - "label": "Delivered Items To Be Billed", - "link_count": 0, - "link_to": "Delivered Items To Be Billed", - "link_type": "Report", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2024-01-18 22:11:51.474477", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Receivables", - "number_cards": [], - "owner": "Administrator", - "parent_page": "Accounting", - "public": 1, - "quick_lists": [], - "restrict_to_domain": "", - "roles": [], - "sequence_id": 4.0, - "shortcuts": [ - { - "color": "Grey", - "doc_view": "List", - "label": "POS Invoice", - "link_to": "POS Invoice", - "stats_filter": "[]", - "type": "DocType" - }, - { - "color": "Grey", - "doc_view": "List", - "label": "Cost Center", - "link_to": "Cost Center", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Sales Invoice", - "link_to": "Sales Invoice", - "stats_filter": "[]", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Journal Entry", - "link_to": "Journal Entry", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Payment Entry", - "link_to": "Payment Entry", - "type": "DocType" - }, - { - "doc_view": "", - "label": "Accounts Receivable", - "link_to": "Accounts Receivable", - "type": "Report" - } - ], - "title": "Receivables" -} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 48b10575b8a..b840b5a56b3 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_import": 1, "autoname": "naming_series:", "creation": "2017-10-23 11:38:54.004355", "doctype": "DocType", @@ -266,7 +267,7 @@ "link_fieldname": "asset_repair" } ], - "modified": "2025-11-28 13:04:34.921098", + "modified": "2026-01-06 15:48:13.862505", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", @@ -280,6 +281,7 @@ "delete": 1, "email": 1, "export": 1, + "import": 1, "print": 1, "read": 1, "report": 1, @@ -295,6 +297,7 @@ "delete": 1, "email": 1, "export": 1, + "import": 1, "print": 1, "read": 1, "report": 1, diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index 26e8d8a2a4a..82c22dac559 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -1,11 +1,12 @@ { + "app": "erpnext", "charts": [ { "chart_name": "Asset Value Analytics", "label": "Asset Value Analytics" } ], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Assets\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Asset Category\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Fixed Asset Register\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "content": "[{\"id\":\"Q-Cl7bMXDm\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"id\":\"gsSQjvl0Tx\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"xRYRq1sW1O\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"Kx2j5N9BKZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"id\":\"jeNsxtLaH3\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"id\":\"EX5e3NvL51\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", "creation": "2020-03-02 15:43:27.634865", "custom_blocks": [], "docstatus": 0, @@ -182,6 +183,7 @@ "link_to": "Asset Maintenance", "link_type": "Report", "onboard": 0, + "report_ref_doctype": "Asset Maintenance", "type": "Link" }, { @@ -193,10 +195,11 @@ "link_to": "Asset Activity", "link_type": "Report", "onboard": 0, + "report_ref_doctype": "Asset Activity", "type": "Link" } ], - "modified": "2024-01-05 17:40:34.570041", + "modified": "2025-12-31 16:22:38.132729", "modified_by": "Administrator", "module": "Assets", "name": "Assets", @@ -208,27 +211,7 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 7.0, - "shortcuts": [ - { - "label": "Asset", - "link_to": "Asset", - "type": "DocType" - }, - { - "label": "Asset Category", - "link_to": "Asset Category", - "type": "DocType" - }, - { - "label": "Fixed Asset Register", - "link_to": "Fixed Asset Register", - "type": "Report" - }, - { - "label": "Dashboard", - "link_to": "Asset", - "type": "Dashboard" - } - ], - "title": "Assets" -} \ No newline at end of file + "shortcuts": [], + "title": "Assets", + "type": "Workspace" +} diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 1ce2dc24d1b..fbe17f3dbbc 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -282,12 +282,13 @@ } ], "grid_page_length": 50, + "hide_toolbar": 1, "icon": "fa fa-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-11-20 12:59:09.925862", + "modified": "2026-01-02 18:16:35.885540", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5eeb25e093b..3fbd00e9196 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -772,7 +772,7 @@ def make_purchase_invoice(source_name, target_doc=None, args=None): @frappe.whitelist() def make_purchase_invoice_from_portal(purchase_order_name): doc = get_mapped_purchase_invoice(purchase_order_name, ignore_permissions=True) - if doc.contact_email != frappe.session.user: + if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() frappe.db.commit() diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 8ebef1571b5..335bb28b0fb 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -34,15 +34,6 @@ frappe.ui.form.on("Request for Quotation", { }); }, - onload: function (frm) { - if (!frm.doc.message_for_supplier) { - frm.set_value( - "message_for_supplier", - __("Please supply the specified items at the best possible rates") - ); - } - }, - refresh: function (frm, cdt, cdn) { if (frm.doc.docstatus === 1) { frm.add_custom_button( @@ -248,6 +239,25 @@ frappe.ui.form.on("Request for Quotation", { } refresh_field("items"); }, + + email_template(frm) { + if (frm.doc.email_template) { + frappe.db + .get_value("Email Template", frm.doc.email_template, [ + "use_html", + "response", + "response_html", + "subject", + ]) + .then((r) => { + frm.set_value( + "message_for_supplier", + r.message.use_html ? r.message.response_html : r.message.response + ); + frm.set_value("subject", r.message.subject); + }); + } + }, preview: (frm) => { let dialog = new frappe.ui.Dialog({ title: __("Preview Email"), diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index fee11e627b4..005078584bb 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -30,6 +30,7 @@ "send_attached_files", "send_document_print", "sec_break_email_2", + "subject", "message_for_supplier", "terms_section_break", "incoterm", @@ -126,6 +127,7 @@ "reqd": 1 }, { + "depends_on": "eval:doc.suppliers.some((item) => item.send_email)", "fieldname": "supplier_response_section", "fieldtype": "Section Break", "label": "Email Details" @@ -139,8 +141,7 @@ }, { "allow_on_submit": 1, - "fetch_from": "email_template.response", - "fetch_if_empty": 1, + "default": "Please supply the specified items at the best possible rates", "fieldname": "message_for_supplier", "fieldtype": "Text Editor", "in_list_view": 1, @@ -251,7 +252,7 @@ "label": "Preview Email" }, { - "depends_on": "eval:!doc.__islocal", + "depends_on": "eval:doc.suppliers.some((item) => item.send_email)", "fieldname": "sec_break_email_2", "fieldtype": "Section Break", "hide_border": 1 @@ -315,6 +316,14 @@ "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 + }, + { + "default": "Request for Quotation", + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject", + "not_nullable": 1, + "reqd": 1 } ], "grid_page_length": 50, @@ -322,7 +331,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-03-03 16:48:39.856779", + "modified": "2026-01-06 10:31:08.747043", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", @@ -393,4 +402,4 @@ "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 2cf564e6152..b2f8a1ed1e5 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -56,6 +56,7 @@ class RequestforQuotation(BuyingController): send_attached_files: DF.Check send_document_print: DF.Check status: DF.Literal["", "Draft", "Submitted", "Cancelled"] + subject: DF.Data suppliers: DF.Table[RequestforQuotationSupplier] tc_name: DF.Link | None terms: DF.TextEditor | None @@ -66,6 +67,7 @@ class RequestforQuotation(BuyingController): def before_validate(self): self.set_has_unit_price_items() self.flags.allow_zero_qty = self.has_unit_price_items + self.set_data_for_supplier() def validate(self): self.validate_duplicate_supplier() @@ -90,6 +92,19 @@ class RequestforQuotation(BuyingController): not row.qty for row in self.get("items") if (row.item_code and not row.qty) ) + def set_data_for_supplier(self): + if self.email_template: + data = frappe.get_value( + "Email Template", + self.email_template, + ["use_html", "response", "response_html", "subject"], + as_dict=True, + ) + if not self.message_for_supplier: + self.message_for_supplier = data.response_html if data.use_html else data.response + if not self.subject: + self.subject = data.subject + def validate_duplicate_supplier(self): supplier_list = [d.supplier for d in self.suppliers] if len(supplier_list) != len(set(supplier_list)): @@ -283,12 +298,6 @@ class RequestforQuotation(BuyingController): } ) - if not self.email_template: - return - - email_template = frappe.get_doc("Email Template", self.email_template) - message = frappe.render_template(email_template.response_, doc_args) - subject = frappe.render_template(email_template.subject, doc_args) fixed_procurement_email = frappe.db.get_single_value("Buying Settings", "fixed_email") if fixed_procurement_email: sender = frappe.db.get_value("Email Account", fixed_procurement_email, "email_id") @@ -296,7 +305,12 @@ class RequestforQuotation(BuyingController): sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None if preview: - return {"message": message, "subject": subject} + return { + "message": self.message_for_supplier, + "subject": self.subject + or frappe.get_value("Email Template", self.email_template, "subject") + or _("Request for Quotation"), + } attachments = [] if self.send_attached_files: @@ -316,7 +330,15 @@ class RequestforQuotation(BuyingController): ) ) - self.send_email(data, sender, subject, message, attachments) + self.send_email( + data, + sender, + self.subject + or frappe.get_value("Email Template", self.email_template, "subject") + or _("Request for Quotation"), + self.message_for_supplier, + attachments, + ) def send_email(self, data, sender, subject, message, attachments): make( diff --git a/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json b/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json index 4019be335cd..34ce6200db2 100644 --- a/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json +++ b/erpnext/buying/doctype/request_for_quotation_supplier/request_for_quotation_supplier.json @@ -80,21 +80,22 @@ "fieldname": "email_id", "fieldtype": "Data", "in_list_view": 1, - "label": "Email Id", + "label": "Email ID", "no_copy": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:10:33.435013", + "modified": "2026-01-05 14:08:27.274538", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Supplier", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index cfed11fabef..f5005fbc12a 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -14,6 +14,7 @@ from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_ from erpnext.accounts.party import ( get_dashboard_info, validate_party_accounts, + validate_party_currency_before_merging, ) from erpnext.controllers.website_list_for_contact import add_role_for_portal_user from erpnext.utilities.transaction_base import TransactionBase @@ -213,6 +214,10 @@ class Supplier(TransactionBase): delete_contact_and_address("Supplier", self.name) + def before_rename(self, olddn, newdn, merge=False): + if merge: + validate_party_currency_before_merging("Supplier", olddn, newdn) + def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": self.db_set("supplier_name", newdn) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index b6a8c7b288c..f3f0ede3e17 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -131,16 +131,14 @@ class TestSupplier(IntegrationTestCase): self.assertEqual(details.tax_category, "_Test Tax Category 1") address = frappe.get_doc( - dict( - doctype="Address", - address_title="_Test Address With Tax Category", - tax_category="_Test Tax Category 2", - address_type="Billing", - address_line1="Station Road", - city="_Test City", - country="India", - links=[dict(link_doctype="Supplier", link_name="_Test Supplier With Tax Category")], - ) + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 2", + address_type="Billing", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Supplier", link_name="_Test Supplier With Tax Category")], ).insert() # Tax Category with Address diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py index e04d1f9c66a..c406739dad7 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/supplier_scorecard_criteria.py @@ -45,7 +45,7 @@ class SupplierScorecardCriteria(Document): mylist = re.finditer(regex, test_formula, re.MULTILINE | re.DOTALL) for _dummy1, match in enumerate(mylist): for _dummy2 in range(0, len(match.groups())): - test_formula = test_formula.replace("{" + match.group(1) + "}", "0") + test_formula = test_formula.replace("{" + match.group(1) + "}", "1") try: frappe.safe_eval(test_formula, None, {"max": max, "min": min}) diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py index 7b98d6d23c0..114ff2b56b3 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py @@ -17,7 +17,6 @@ class TestSupplierScorecardCriteria(IntegrationTestCase): def test_formula_validate(self): delete_test_scorecards() self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[1]).insert) - self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[2]).insert) def delete_test_scorecards(): @@ -68,16 +67,8 @@ test_bad_criteria = [ "name": "Fake Criteria 2", "weight": 40.0, "doctype": "Supplier Scorecard Criteria", - "formula": "(({cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Force 0 divided by 0 + "formula": "(({cost_of_on_time_shipments} {cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Two variables beside eachother "criteria_name": "Fake Criteria 2", "max_score": 100.0, }, - { - "name": "Fake Criteria 3", - "weight": 40.0, - "doctype": "Supplier Scorecard Criteria", - "formula": "(({cost_of_on_time_shipments} {cost_of_on_time_shipments} / {tot_cost_shipments}))* 100", # Two variables beside eachother - "criteria_name": "Fake Criteria 3", - "max_score": 100.0, - }, ] diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js index 2651023639e..90d9d6a0412 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js @@ -97,7 +97,7 @@ frappe.query_reports["Purchase Order Analysis"] = { value = default_formatter(value, row, column, data); let format_fields = ["received_qty", "billed_amount"]; - if (in_list(format_fields, column.fieldname) && data && data[column.fieldname] > 0) { + if (format_fields.includes(column.fieldname) && data && data[column.fieldname] > 0) { value = "" + value + ""; } return value; diff --git a/erpnext/buying/workspace/buying/buying.json b/erpnext/buying/workspace/buying/buying.json index 18d86da9ea5..4f356256f11 100644 --- a/erpnext/buying/workspace/buying/buying.json +++ b/erpnext/buying/workspace/buying/buying.json @@ -6,7 +6,7 @@ "label": "Purchase Order Trends" } ], - "content": "[{\"id\":\"j3dJGo8Ok6\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"id\":\"k75jSq2D6Z\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Purchase Orders Count\",\"col\":4}},{\"id\":\"UPXys0lQLj\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Purchase Amount\",\"col\":4}},{\"id\":\"yQGK3eb2hg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Values\",\"col\":4}},{\"id\":\"oN7lXSwQji\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Ivw1PI_wEJ\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"RrWFEi4kCf\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"RFIakryyJP\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"id\":\"bM10abFmf6\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order\",\"col\":3}},{\"id\":\"lR0Hw_37Pu\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Analytics\",\"col\":3}},{\"id\":\"_HN0Ljw1lX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Order Analysis\",\"col\":3}},{\"id\":\"kuLuiMRdnX\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"tQFeiKptW2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Procurement\",\"col\":3}},{\"id\":\"0NiuFE_EGS\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Xe2GVLOq8J\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"QwqyG6XuUt\",\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"id\":\"bTPjOxC_N_\",\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"id\":\"87ht0HIneb\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"EDOsBOmwgw\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"id\":\"oWNNIiNb2i\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"id\":\"7F_13-ihHB\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"pfwiLvionl\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"id\":\"8ySDy6s4qn\",\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]", + "content": "[{\"id\":\"j3dJGo8Ok6\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"id\":\"k75jSq2D6Z\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Purchase Orders Count\",\"col\":4}},{\"id\":\"UPXys0lQLj\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Purchase Amount\",\"col\":4}},{\"id\":\"yQGK3eb2hg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Values\",\"col\":4}},{\"id\":\"oN7lXSwQji\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Xe2GVLOq8J\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"QwqyG6XuUt\",\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"id\":\"bTPjOxC_N_\",\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"id\":\"87ht0HIneb\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"EDOsBOmwgw\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"id\":\"oWNNIiNb2i\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"id\":\"7F_13-ihHB\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"pfwiLvionl\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"id\":\"8ySDy6s4qn\",\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]", "creation": "2020-01-28 11:50:26.195467", "custom_blocks": [], "docstatus": 0, @@ -512,7 +512,7 @@ "type": "Link" } ], - "modified": "2025-12-19 16:12:02.461082", + "modified": "2026-01-02 14:55:59.078773", "modified_by": "Administrator", "module": "Buying", "name": "Buying", @@ -537,56 +537,7 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 5.0, - "shortcuts": [ - { - "color": "Green", - "format": "{} Available", - "label": "Item", - "link_to": "Item", - "stats_filter": "{\n \"disabled\": 0\n}", - "type": "DocType" - }, - { - "color": "Grey", - "doc_view": "List", - "label": "Learn Procurement", - "type": "URL", - "url": "https://school.frappe.io/lms/courses/procurement?utm_source=in_app" - }, - { - "color": "Yellow", - "format": "{} Pending", - "label": "Material Request", - "link_to": "Material Request", - "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Pending\"\n}", - "type": "DocType" - }, - { - "color": "Yellow", - "format": "{} To Receive", - "label": "Purchase Order", - "link_to": "Purchase Order", - "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\":[\"in\", [\"To Receive\", \"To Receive and Bill\"]]\n}", - "type": "DocType" - }, - { - "label": "Purchase Analytics", - "link_to": "Purchase Analytics", - "report_ref_doctype": "Purchase Order", - "type": "Report" - }, - { - "label": "Purchase Order Analysis", - "link_to": "Purchase Order Analysis", - "report_ref_doctype": "Purchase Order", - "type": "Report" - }, - { - "label": "Dashboard", - "link_to": "Buying", - "type": "Dashboard" - } - ], + "shortcuts": [], "title": "Buying", "type": "Workspace" } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 59810544c0c..73cc888fc4a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3748,9 +3748,9 @@ def validate_child_on_delete(row, parent, ordered_item=None): ) if flt(row.ordered_qty): frappe.throw( - _("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format( - row.idx, row.item_code - ) + _( + "Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order." + ).format(row.idx, row.item_code) ) if parent.doctype == "Purchase Order" and flt(row.received_qty): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a0d51a0c016..837bbbc793c 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -51,7 +51,7 @@ class BuyingController(SubcontractingController): self.validate_purchase_receipt_if_update_stock() if self.doctype == "Purchase Receipt" or (self.doctype == "Purchase Invoice" and self.update_stock): - # self.validate_purchase_return() + self.validate_purchase_return() self.validate_rejected_warehouse() self.validate_accepted_rejected_qty() validate_for_items(self) @@ -682,15 +682,8 @@ class BuyingController(SubcontractingController): def validate_purchase_return(self): for d in self.get("items"): - if self.is_return and flt(d.rejected_qty) != 0: - frappe.throw( - _("Row #{idx}: {field_label} is not allowed in Purchase Return.").format( - idx=d.idx, - field_label=_(d.meta.get_label("rejected_qty")), - ) - ) - - # validate rate with ref PR + if self.is_return and not flt(d.rejected_qty) and d.rejected_warehouse: + d.rejected_warehouse = None # validate accepted and rejected qty def validate_accepted_rejected_qty(self): diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index ad185228a47..77599e3a009 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -188,7 +188,7 @@ def find_variant(template, args, variant_item_code=None): for attribute, value in args.items(): for row in variant.attributes: - if row.attribute == attribute and row.attribute_value == cstr(value): + if row.attribute == _(attribute) and row.attribute_value == cstr(value): # this row matches match_count += 1 break @@ -209,7 +209,7 @@ def create_variant(item, args, use_template_image=False): variant_attributes = [] for d in template.attributes: - variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(d.attribute)}) + variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(_(d.attribute))}) variant.set("attributes", variant_attributes) copy_attributes_to_variant(template, variant) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 09a42e79d4a..b4f618aecaa 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -7,7 +7,7 @@ import frappe from frappe import _, bold from frappe.model.meta import get_field_precision from frappe.query_builder import DocType -from frappe.query_builder.functions import Abs +from frappe.query_builder.functions import Abs, Sum from frappe.utils import cint, flt, format_datetime, get_datetime import erpnext @@ -313,6 +313,68 @@ def get_already_returned_items(doc): return items +def get_returned_qty_map_for_purchase_flow(return_against, supplier, row_name, doctype): + # return map of warehouses with qty and stock qty + # Example: {'_Test Rejected Warehouse - _TC': {'qty': 5.0, 'stock_qty': 5.0}, '_Test Warehouse - _TC': {'qty': 8.0, 'stock_qty': 8.0}} + + parent_doc = frappe.qb.DocType(doctype) + child_doc = frappe.qb.DocType(doctype + " Item") + + query = ( + frappe.qb.from_(parent_doc) + .inner_join(child_doc) + .on(child_doc.parent == parent_doc.name) + .select( + child_doc.qty, + child_doc.rejected_qty, + child_doc.warehouse, + child_doc.rejected_warehouse, + child_doc.conversion_factor, + ) + .where( + (parent_doc.return_against == return_against) + & (parent_doc.supplier == supplier) + & (parent_doc.docstatus == 1) + & (parent_doc.is_return == 1) + ) + ) + + if doctype != "Subcontracting Receipt": + query = query.select(child_doc.stock_qty) + + doctype_field_map = { + "Purchase Receipt": child_doc.purchase_receipt_item, + "Subcontracting Receipt": child_doc.subcontracting_receipt_item, + } + + field = doctype_field_map.get(doctype) + if field: + query = query.where(field == row_name) + + data = query.run(as_dict=True) + + _return_map = frappe._dict({}) + + for row in data: + if row.warehouse and row.warehouse not in _return_map: + _return_map[row.warehouse] = frappe._dict({"qty": 0, "stock_qty": 0}) + + if row.rejected_warehouse and row.rejected_warehouse not in _return_map: + _return_map[row.rejected_warehouse] = frappe._dict({"qty": 0, "stock_qty": 0}) + + if row.warehouse: + qty_map = _return_map.get(row.warehouse) + qty_map.qty += abs(flt(row.qty)) + qty_map.stock_qty += abs(flt(row.stock_qty)) + + if row.rejected_warehouse: + rejected_qty_map = _return_map.get(row.rejected_warehouse) + rejected_qty_map.qty += abs(flt(row.rejected_qty)) + rejected_qty_map.stock_qty += abs(flt(row.rejected_qty) * flt(row.conversion_factor)) + + return _return_map + + def get_returned_qty_map_for_row(return_against, party, row_name, doctype): child_doctype = doctype + " Item" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) @@ -459,29 +521,22 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai target_doc.pricing_rules = None if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: - returned_qty_map = get_returned_qty_map_for_row( + returned_qty_map = get_returned_qty_map_for_purchase_flow( source_parent.name, source_parent.supplier, source_doc.name, doctype ) + wh_map = returned_qty_map.get(source_doc.warehouse) or frappe._dict() + rejected_wh_map = returned_qty_map.get(source_doc.rejected_warehouse) or frappe._dict() + if doctype == "Subcontracting Receipt": target_doc.received_qty = -1 * flt(source_doc.qty) else: - target_doc.received_qty = -1 * flt( - source_doc.received_qty - (returned_qty_map.get("received_qty") or 0) - ) - target_doc.rejected_qty = -1 * flt( - source_doc.rejected_qty - (returned_qty_map.get("rejected_qty") or 0) - ) + target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (rejected_wh_map.qty or 0)) - target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) + target_doc.qty = -1 * flt(source_doc.qty - (wh_map.qty or 0)) if hasattr(target_doc, "stock_qty") and not return_against_rejected_qty: - target_doc.stock_qty = -1 * flt( - source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0) - ) - target_doc.received_stock_qty = -1 * flt( - source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0) - ) + target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (flt(wh_map.stock_qty) or 0)) if doctype == "Subcontracting Receipt": target_doc.subcontracting_order = source_doc.subcontracting_order @@ -489,7 +544,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.subcontracting_receipt_item = source_doc.name if return_against_rejected_qty: - target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0)) + target_doc.qty = -1 * flt(source_doc.rejected_qty - (rejected_wh_map.qty or 0)) target_doc.rejected_qty = 0.0 target_doc.rejected_warehouse = "" target_doc.warehouse = source_doc.rejected_warehouse @@ -502,7 +557,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai target_doc.purchase_receipt_item = source_doc.name if doctype == "Purchase Receipt" and return_against_rejected_qty: - target_doc.qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get("qty") or 0)) + target_doc.qty = -1 * flt(source_doc.rejected_qty - (rejected_wh_map.qty or 0)) target_doc.rejected_qty = 0.0 target_doc.rejected_warehouse = "" target_doc.warehouse = source_doc.rejected_warehouse @@ -580,6 +635,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai ): target_doc.set("use_serial_batch_fields", 1) + if ( + not source_doc.serial_no + and not source_doc.batch_no + and source_doc.serial_and_batch_bundle + and source_doc.use_serial_batch_fields + ): + target_doc.set("use_serial_batch_fields", 0) + if source_doc.item_code and target_doc.get("use_serial_batch_fields"): item_details = frappe.get_cached_value( "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1 diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d7d12ff46bb..57aeea727bc 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -1022,10 +1022,19 @@ class SellingController(StockController): def set_default_income_account_for_item(obj): - for d in obj.get("items"): - if d.item_code: - if getattr(d, "income_account", None): - set_item_default(d.item_code, obj.company, "income_account", d.income_account) + """Set income account as default for items in the transaction. + + Updates the item default income account for each item in the transaction + if it differs from the company's default income account. + + Args: + obj: Transaction document containing items table with income_account field + """ + company_default = frappe.get_cached_value("Company", obj.company, "default_income_account") + for d in obj.get("items", default=[]): + income_account = getattr(d, "income_account", None) + if d.item_code and income_account and income_account != company_default: + set_item_default(d.item_code, obj.company, "income_account", income_account) def get_serial_and_batch_bundle(child, parent, delivery_note_child=None): diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index b12741487cd..652ce697711 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -184,6 +184,9 @@ class StatusUpdater(Document): Installation Note: Update Installed Qty, Update Percent Qty and Validate over installation """ + def on_discard(self): + self.db_set("status", "Cancelled") + def update_prevdoc_status(self): self.update_qty() self.validate_qty() diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index a6af04f989a..c6daf7ead01 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -1401,6 +1401,7 @@ def make_rm_stock_entry( stock_entry.set_stock_entry_type() + over_transfer_allowance = frappe.get_single_value("Buying Settings", "over_transfer_allowance") for fg_item_code in fg_item_code_list: for rm_item in rm_items: if ( @@ -1408,14 +1409,27 @@ def make_rm_stock_entry( or rm_item.get("item_code") == fg_item_code ): rm_item_code = rm_item.get("rm_item_code") + qty = rm_item.get("qty") or max( + rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0 + ) + if qty <= 0 and rm_item.get("total_supplied_qty"): + per_transferred = ( + flt( + rm_item.get("total_supplied_qty") / rm_item.get("required_qty"), + frappe.db.get_default("float_precision"), + ) + * 100 + ) + if per_transferred >= 100 + over_transfer_allowance: + continue + items_dict = { rm_item_code: { rm_detail_field: rm_item.get("name"), "item_name": rm_item.get("item_name") or item_wh.get(rm_item_code, {}).get("item_name", ""), "description": item_wh.get(rm_item_code, {}).get("description", ""), - "qty": rm_item.get("qty") - or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0), + "qty": qty, "from_warehouse": rm_item.get("warehouse") or rm_item.get("reserve_warehouse"), "to_warehouse": subcontract_order.supplier_warehouse, diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 20c6b9f56c6..d1f70af86f1 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -101,9 +101,11 @@ "label": "Success Redirect URL" } ], + "grid_page_length": 50, + "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:05:59.465023", + "modified": "2026-01-02 18:18:46.617101", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", @@ -140,8 +142,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 9997f97dcc8..9ef01283c31 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -17,9 +17,7 @@ class AppointmentBookingSettings(Document): from typing import TYPE_CHECKING if TYPE_CHECKING: - from frappe.automation.doctype.assignment_rule_user.assignment_rule_user import ( - AssignmentRuleUser, - ) + from frappe.automation.doctype.assignment_rule_user.assignment_rule_user import AssignmentRuleUser from frappe.types import DF from erpnext.crm.doctype.appointment_booking_slots.appointment_booking_slots import ( diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index c5014542376..2d7ace852cc 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -85,7 +85,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Unsigned\nActive\nInactive" + "options": "Unsigned\nActive\nInactive\nCancelled" }, { "allow_on_submit": 1, @@ -257,11 +257,11 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], - "modified": "2025-06-19 17:48:45.049007", + "modified": "2025-12-24 21:33:51.240497", "modified_by": "Administrator", "module": "CRM", "name": "Contract", - "naming_rule": "Expression (old style)", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 223e0549ede..43fc23bb5c6 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -43,7 +43,7 @@ class Contract(Document): signed_on: DF.Datetime | None signee: DF.Data | None start_date: DF.Date | None - status: DF.Literal["Unsigned", "Active", "Inactive"] + status: DF.Literal["Unsigned", "Active", "Inactive", "Cancelled"] # end: auto-generated types def validate(self): @@ -61,6 +61,9 @@ class Contract(Document): def before_submit(self): self.signed_by_company = frappe.session.user + def on_discard(self): + self.db_set("status", "Cancelled") + def before_update_after_submit(self): self.update_contract_status() self.update_fulfilment_status() diff --git a/erpnext/crm/doctype/crm_settings/crm_settings.json b/erpnext/crm/doctype/crm_settings/crm_settings.json index 40ed9a7f2c6..3f231d460ab 100644 --- a/erpnext/crm/doctype/crm_settings/crm_settings.json +++ b/erpnext/crm/doctype/crm_settings/crm_settings.json @@ -100,11 +100,13 @@ "label": "Update timestamp on new communication" } ], + "grid_page_length": 50, + "hide_toolbar": 1, "icon": "fa fa-cog", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-01-16 16:12:14.889455", + "modified": "2026-01-02 18:18:52.204988", "modified_by": "Administrator", "module": "CRM", "name": "CRM Settings", @@ -140,8 +142,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index d2fc4ca849b..4f041d8da4b 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -153,15 +153,14 @@ class OpportunitySummaryBySalesStage: }[self.filters.get("based_on")] if self.filters.get("based_on") == "Opportunity Owner": - if ( - d.get(based_on) == "[]" - or d.get(based_on) is None - or d.get(based_on) == "Not Assigned" - or d.get(based_on) == "" - ): + value = d.get(based_on) + if not value or value in ["[]", "null", "Not Assigned"]: assignments = ["Not Assigned"] else: - assignments = json.loads(d.get(based_on)) + try: + assignments = json.loads(value) + except json.JSONDecodeError: + assignments = ["Not Assigned"] sales_stage = d.get("sales_stage") count = d.get(data_based_on) diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index 15db21446b2..59814c87e4a 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -1,11 +1,12 @@ { + "app": "erpnext", "charts": [ { - "chart_name": "Territory Wise Sales", - "label": "Territory Wise Sales" + "chart_name": "Won Opportunities", + "label": "Won Opportunities" } ], - "content": "[{\"id\":\"Cj2TyhgiWy\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"id\":\"LAKRmpYMRA\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"XGIwEUStw_\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"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}}]", + "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}}]", "creation": "2020-01-23 14:48:30.183272", "custom_blocks": [], "docstatus": 0, @@ -17,14 +18,6 @@ "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, @@ -122,14 +115,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Maintenance", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, { "dependencies": "", "hidden": 0, @@ -163,183 +148,6 @@ "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, @@ -420,11 +228,24 @@ "type": "Link" } ], - "modified": "2024-09-12 04:43:41.406488", + "modified": "2026-01-03 15:05:23.983099", "modified_by": "Administrator", "module": "CRM", "name": "CRM", - "number_cards": [], + "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)" + } + ], "owner": "Administrator", "parent_page": "", "public": 1, @@ -432,38 +253,7 @@ "restrict_to_domain": "", "roles": [], "sequence_id": 17.0, - "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", - "type": "Report" - }, - { - "label": "Dashboard", - "link_to": "CRM", - "type": "Dashboard" - } - ], - "title": "CRM" -} \ No newline at end of file + "shortcuts": [], + "title": "CRM", + "type": "Workspace" +} diff --git a/erpnext/desktop_icon/accounting.json b/erpnext/desktop_icon/accounting.json index 3d33bea7a3b..72f393c14f1 100644 --- a/erpnext/desktop_icon/accounting.json +++ b/erpnext/desktop_icon/accounting.json @@ -1,6 +1,6 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.309749", + "creation": "2025-11-17 20:55:11.854086", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, @@ -9,12 +9,13 @@ "idx": 1, "label": "Accounting", "link_to": "Accounting", - "link_type": "Workspace", - "modified": "2025-11-17 13:33:35.788242", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.203651", "modified_by": "Administrator", "name": "Accounting", "owner": "Administrator", "parent_icon": "Accounts", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/assets.json b/erpnext/desktop_icon/assets.json index abda0f8b9c3..b9b52466a08 100644 --- a/erpnext/desktop_icon/assets.json +++ b/erpnext/desktop_icon/assets.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.299276", + "creation": "2025-11-17 20:55:11.845676", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "assets", "icon_type": "Link", - "idx": 8, + "idx": 1, "label": "Assets", "link_to": "Assets", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/asset.svg", - "modified": "2025-11-17 17:41:41.635533", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.220411", "modified_by": "Administrator", "name": "Assets", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/banking.json b/erpnext/desktop_icon/banking.json index 34308c086ba..71a36f21da4 100644 --- a/erpnext/desktop_icon/banking.json +++ b/erpnext/desktop_icon/banking.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 5, "label": "Banking", - "link_to": "Bank Reconciliation Tool", - "link_type": "DocType", - "modified": "2025-11-19 15:57:20.139306", + "link_to": "Banking", + "link_type": "Workspace", + "modified": "2026-01-02 13:03:29.270503", "modified_by": "Administrator", "name": "Banking", "owner": "Administrator", "parent_icon": "Accounts", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/budget.json b/erpnext/desktop_icon/budget.json index c8ada5de2a9..0404f3eaa40 100644 --- a/erpnext/desktop_icon/budget.json +++ b/erpnext/desktop_icon/budget.json @@ -9,12 +9,13 @@ "idx": 4, "label": "Budget", "link_to": "Budget", - "link_type": "DocType", - "modified": "2025-11-17 13:34:13.514949", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.449176", "modified_by": "Administrator", "name": "Budget", "owner": "Administrator", "parent_icon": "Accounts", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/buying.json b/erpnext/desktop_icon/buying.json index 9b195c6305b..64d6712defe 100644 --- a/erpnext/desktop_icon/buying.json +++ b/erpnext/desktop_icon/buying.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.327790", + "creation": "2025-11-17 20:55:11.868134", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "buying", "icon_type": "Link", - "idx": 3, + "idx": 1, "label": "Buying", "link_to": "Buying", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/buying.svg", - "modified": "2025-11-17 17:38:19.203107", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.196163", "modified_by": "Administrator", "name": "Buying", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/crm.json b/erpnext/desktop_icon/crm.json index 046920f37c8..d9dbe85cac2 100644 --- a/erpnext/desktop_icon/crm.json +++ b/erpnext/desktop_icon/crm.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.340610", + "creation": "2025-11-17 20:55:11.876996", "docstatus": 0, "doctype": "Desktop Icon", - "hidden": 0, + "hidden": 1, "icon": "crm", "icon_type": "Link", - "idx": 7, + "idx": 1, "label": "CRM", "link_to": "CRM", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/crm.svg", - "modified": "2025-11-17 19:39:59.734778", + "link_type": "Workspace Sidebar", + "modified": "2026-01-06 14:54:05.112927", "modified_by": "Administrator", "name": "CRM", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/financial_reports.json b/erpnext/desktop_icon/financial_reports.json index 1e6f5dbbbfb..e4425ad1544 100644 --- a/erpnext/desktop_icon/financial_reports.json +++ b/erpnext/desktop_icon/financial_reports.json @@ -1,20 +1,22 @@ { "app": "erpnext", - "creation": "2025-11-05 12:11:24.655043", + "creation": "2025-11-17 20:55:11.772622", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "file", "icon_type": "Link", - "idx": 7, + "idx": 0, "label": "Financial Reports", "link_to": "Financial Reports", - "link_type": "Workspace", - "modified": "2025-11-17 13:34:48.074533", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.253367", "modified_by": "Administrator", "name": "Financial Reports", "owner": "Administrator", "parent_icon": "Accounts", + "restrict_removal": 0, "roles": [], + "sidebar": "", "standard": 1 } diff --git a/erpnext/desktop_icon/home.json b/erpnext/desktop_icon/home.json index ab4014de489..7245b5fe0c7 100644 --- a/erpnext/desktop_icon/home.json +++ b/erpnext/desktop_icon/home.json @@ -9,12 +9,13 @@ "idx": 0, "label": "Home", "link_to": "Home", - "link_type": "Workspace", - "modified": "2025-11-20 16:09:28.269913", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.174950", "modified_by": "Administrator", "name": "Home", "owner": "Administrator", "parent_icon": "", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/manufacturing.json b/erpnext/desktop_icon/manufacturing.json index 08cbc55fb69..1f610094cac 100644 --- a/erpnext/desktop_icon/manufacturing.json +++ b/erpnext/desktop_icon/manufacturing.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.269805", + "creation": "2025-11-17 20:55:11.824443", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "organization", "icon_type": "Link", - "idx": 5, + "idx": 1, "label": "Manufacturing", "link_to": "Manufacturing", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/manufacturing.svg", - "modified": "2025-11-17 17:40:14.207766", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.246693", "modified_by": "Administrator", "name": "Manufacturing", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/opening_&_closing.json b/erpnext/desktop_icon/opening_&_closing.json deleted file mode 100644 index 9b3e96b6d32..00000000000 --- a/erpnext/desktop_icon/opening_&_closing.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-12 15:15:15.824801", - "docstatus": 0, - "doctype": "Desktop Icon", - "hidden": 0, - "icon": "panel-top-open", - "icon_type": "Link", - "idx": 2, - "label": "Opening & Closing", - "link_to": "Period Closing Voucher", - "link_type": "DocType", - "modified": "2025-11-19 15:59:14.805915", - "modified_by": "Administrator", - "name": "Opening & Closing", - "owner": "Administrator", - "parent_icon": "Accounts", - "roles": [], - "standard": 1 -} diff --git a/erpnext/desktop_icon/payables.json b/erpnext/desktop_icon/payables.json deleted file mode 100644 index 0c8064d4d2c..00000000000 --- a/erpnext/desktop_icon/payables.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-17 13:19:04.255041", - "docstatus": 0, - "doctype": "Desktop Icon", - "hidden": 0, - "icon": "arrow-left", - "icon_type": "Link", - "idx": 0, - "label": "Payables", - "link_to": "Payables", - "link_type": "Workspace", - "modified": "2025-11-17 15:53:14.737052", - "modified_by": "Administrator", - "name": "Payables", - "owner": "Administrator", - "parent_icon": "Accounts", - "roles": [], - "standard": 1 -} diff --git a/erpnext/desktop_icon/projects.json b/erpnext/desktop_icon/projects.json index 8ec9091c0a7..2fc1e054f6b 100644 --- a/erpnext/desktop_icon/projects.json +++ b/erpnext/desktop_icon/projects.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.293376", + "creation": "2025-11-17 20:55:11.841426", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "project", "icon_type": "Link", - "idx": 9, + "idx": 1, "label": "Projects", "link_to": "Projects", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/projects.svg", - "modified": "2025-11-17 17:42:29.470661", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.226383", "modified_by": "Administrator", "name": "Projects", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/quality.json b/erpnext/desktop_icon/quality.json index 8e35689bcbc..d9b036198e3 100644 --- a/erpnext/desktop_icon/quality.json +++ b/erpnext/desktop_icon/quality.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.281193", + "creation": "2025-11-17 20:55:11.828716", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "quality", "icon_type": "Link", - "idx": 9, + "idx": 1, "label": "Quality", "link_to": "Quality", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/quality.svg", - "modified": "2025-11-17 17:42:48.371889", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.239523", "modified_by": "Administrator", "name": "Quality", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/receivables.json b/erpnext/desktop_icon/receivables.json deleted file mode 100644 index ebf59afd781..00000000000 --- a/erpnext/desktop_icon/receivables.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-17 13:19:04.249828", - "docstatus": 0, - "doctype": "Desktop Icon", - "hidden": 0, - "icon": "arrow-right", - "icon_type": "Link", - "idx": 0, - "label": "Receivables", - "link_to": "Receivables", - "link_type": "Workspace", - "modified": "2025-11-17 15:53:24.958812", - "modified_by": "Administrator", - "name": "Receivables", - "owner": "Administrator", - "parent_icon": "Accounts", - "roles": [], - "standard": 1 -} diff --git a/erpnext/desktop_icon/selling.json b/erpnext/desktop_icon/selling.json index 6b1b7940ba6..f0041fe0116 100644 --- a/erpnext/desktop_icon/selling.json +++ b/erpnext/desktop_icon/selling.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.335051", + "creation": "2025-11-17 20:55:11.872574", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "sell", "icon_type": "Link", - "idx": 2, + "idx": 1, "label": "Selling", "link_to": "Selling", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/selling.svg", - "modified": "2025-11-17 17:37:59.847865", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.189446", "modified_by": "Administrator", "name": "Selling", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/settings.json b/erpnext/desktop_icon/settings.json index 664c0dbea14..484fd57cd38 100644 --- a/erpnext/desktop_icon/settings.json +++ b/erpnext/desktop_icon/settings.json @@ -9,13 +9,14 @@ "idx": 10, "label": "Settings", "link_to": "Settings", - "link_type": "Workspace", + "link_type": "Workspace Sidebar", "logo_url": "/assets/erpnext/desktop_icons/settings.svg", - "modified": "2025-11-17 19:40:12.175977", + "modified": "2026-01-01 20:07:01.330786", "modified_by": "Administrator", "name": "Settings", "owner": "Administrator", "parent_icon": "", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/stock.json b/erpnext/desktop_icon/stock.json index ed1525a5acd..a2488c36c13 100644 --- a/erpnext/desktop_icon/stock.json +++ b/erpnext/desktop_icon/stock.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.304517", + "creation": "2025-11-17 20:55:11.849904", "docstatus": 0, "doctype": "Desktop Icon", "hidden": 0, "icon": "stock", "icon_type": "Link", - "idx": 4, + "idx": 1, "label": "Stock", "link_to": "Stock", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/stock.svg", - "modified": "2025-11-17 17:38:40.957627", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.212940", "modified_by": "Administrator", "name": "Stock", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/subcontracting.json b/erpnext/desktop_icon/subcontracting.json index 8c33d2027c4..e84b34f9d88 100644 --- a/erpnext/desktop_icon/subcontracting.json +++ b/erpnext/desktop_icon/subcontracting.json @@ -9,13 +9,14 @@ "idx": 6, "label": "Subcontracting", "link_to": "Subcontracting", - "link_type": "Workspace", + "link_type": "Workspace Sidebar", "logo_url": "/assets/erpnext/desktop_icons/subcontracting.svg", - "modified": "2025-11-17 19:39:41.287788", + "modified": "2026-01-01 20:07:01.323508", "modified_by": "Administrator", "name": "Subcontracting", "owner": "Administrator", "parent_icon": "", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/subscription.json b/erpnext/desktop_icon/subscription.json index be04b0398f7..00dd5d7d374 100644 --- a/erpnext/desktop_icon/subscription.json +++ b/erpnext/desktop_icon/subscription.json @@ -6,15 +6,16 @@ "hidden": 0, "icon": "monitor-check", "icon_type": "Link", - "idx": 6, + "idx": 99, "label": "Subscription", "link_to": "Subscription", - "link_type": "DocType", - "modified": "2025-11-19 16:02:32.686833", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.548581", "modified_by": "Administrator", "name": "Subscription", "owner": "Administrator", "parent_icon": "Accounts", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/support.json b/erpnext/desktop_icon/support.json index 511769ae247..873b2a798b0 100644 --- a/erpnext/desktop_icon/support.json +++ b/erpnext/desktop_icon/support.json @@ -1,21 +1,21 @@ { "app": "erpnext", - "creation": "2025-11-17 13:19:04.288298", + "creation": "2025-11-17 20:55:11.837227", "docstatus": 0, "doctype": "Desktop Icon", - "hidden": 0, + "hidden": 1, "icon": "support", "icon_type": "Link", - "idx": 10, + "idx": 1, "label": "Support", "link_to": "Support", - "link_type": "Workspace", - "logo_url": "/assets/erpnext/desktop_icons/support.svg", - "modified": "2025-11-17 17:43:09.335445", + "link_type": "Workspace Sidebar", + "modified": "2026-01-06 14:53:54.100467", "modified_by": "Administrator", "name": "Support", "owner": "Administrator", - "parent_icon": "", + "parent_icon": "ERPNext", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/erpnext/desktop_icon/taxes.json b/erpnext/desktop_icon/taxes.json index 69ffd6c7568..80f27387e78 100644 --- a/erpnext/desktop_icon/taxes.json +++ b/erpnext/desktop_icon/taxes.json @@ -8,13 +8,15 @@ "icon_type": "Link", "idx": 3, "label": "Taxes", - "link_to": "Item Tax Template", - "link_type": "DocType", - "modified": "2025-11-19 15:58:21.226664", + "link_to": "Taxes", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.356333", "modified_by": "Administrator", "name": "Taxes", "owner": "Administrator", "parent_icon": "Accounts", + "restrict_removal": 0, "roles": [], + "sidebar": "", "standard": 1 } diff --git a/erpnext/locale/ar.po b/erpnext/locale/ar.po index 550e6a63a15..12f5ea1f128 100644 --- a/erpnext/locale/ar.po +++ b/erpnext/locale/ar.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: hello@frappe.io\n" -"POT-Creation-Date: 2025-12-14 09:37+0000\n" -"PO-Revision-Date: 2025-12-15 01:40\n" +"POT-Creation-Date: 2025-12-21 09:37+0000\n" +"PO-Revision-Date: 2025-12-22 03:07\n" "Last-Translator: hello@frappe.io\n" "Language-Team: Arabic\n" "MIME-Version: 1.0\n" @@ -590,7 +590,7 @@ msgstr "" msgid "90 Above" msgstr "" -#: erpnext/assets/doctype/asset/asset.py:474 +#: erpnext/assets/doctype/asset/asset.py:533 msgid "Cannot create asset.Cannot overbill for the following Items:
" @@ -824,9 +824,7 @@ msgid "Quick Access" msgstr "" #. Header text in the Assets Workspace -#. Header text in the Quality Workspace #: erpnext/assets/workspace/assets/assets.json -#: erpnext/quality_management/workspace/quality/quality.json msgid "Reports & Masters" msgstr "Jelentések és Törzsadatok" @@ -837,9 +835,9 @@ msgstr "Jelentések és Törzsadatok" #. Header text in the CRM Workspace #. Header text in the Manufacturing Workspace #. Header text in the Projects Workspace +#. Header text in the Quality Workspace #. Header text in the Selling Workspace #. Header text in the Home Workspace -#. Header text in the Subcontracting Workspace #. Header text in the Support Workspace #: erpnext/accounts/workspace/accounting/accounting.json #: erpnext/accounts/workspace/payables/payables.json @@ -848,9 +846,9 @@ msgstr "Jelentések és Törzsadatok" #: erpnext/crm/workspace/crm/crm.json #: erpnext/manufacturing/workspace/manufacturing/manufacturing.json #: erpnext/projects/workspace/projects/projects.json +#: erpnext/quality_management/workspace/quality/quality.json #: erpnext/selling/workspace/selling/selling.json #: erpnext/setup/workspace/home/home.json -#: erpnext/subcontracting/workspace/subcontracting/subcontracting.json #: erpnext/support/workspace/support/support.json msgid "Reports & Masters" msgstr "Jelentések & Törzsadatok" @@ -862,6 +860,11 @@ msgstr "Jelentések & Törzsadatok" msgid "Shortcuts" msgstr "" +#. Header text in the Subcontracting Workspace +#: erpnext/subcontracting/workspace/subcontracting/subcontracting.json +msgid "Subcontracting Inward and Outward" +msgstr "" + #. Header text in the Settings Workspace #: erpnext/setup/workspace/settings/settings.json msgid "Your Shortcuts\n" @@ -880,7 +883,6 @@ msgstr "" #. Header text in the Projects Workspace #. Header text in the Quality Workspace #. Header text in the Home Workspace -#. Header text in the Subcontracting Workspace #. Header text in the Support Workspace #: erpnext/assets/workspace/assets/assets.json #: erpnext/buying/workspace/buying/buying.json @@ -889,16 +891,15 @@ msgstr "" #: erpnext/projects/workspace/projects/projects.json #: erpnext/quality_management/workspace/quality/quality.json #: erpnext/setup/workspace/home/home.json -#: erpnext/subcontracting/workspace/subcontracting/subcontracting.json #: erpnext/support/workspace/support/support.json msgid "Your Shortcuts" msgstr "Hivatkozásai" -#: erpnext/accounts/doctype/payment_request/payment_request.py:995 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1002 msgid "Grand Total: {0}" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:996 +#: erpnext/accounts/doctype/payment_request/payment_request.py:1003 msgid "Outstanding Amount: {0}" msgstr "" @@ -1138,7 +1139,7 @@ msgstr "" #. Label of the qty (Float) field in DocType 'Purchase Receipt Item' #. Label of the qty (Float) field in DocType 'Subcontracting Receipt Item' -#: erpnext/public/js/controllers/transaction.js:2799 +#: erpnext/public/js/controllers/transaction.js:2795 #: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json #: erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json msgid "Accepted Quantity" @@ -1171,7 +1172,7 @@ msgstr "" msgid "According to CEFACT/ICG/2010/IC013 or CEFACT/ICG/2010/IC010" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:938 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:939 msgid "According to the BOM {0}, the Item '{1}' is missing in the stock entry." msgstr "" @@ -1406,7 +1407,7 @@ msgstr "" msgid "Account is not set for the dashboard chart {0}" msgstr "" -#: erpnext/assets/doctype/asset/asset.py:824 +#: erpnext/assets/doctype/asset/asset.py:883 msgid "Account not Found" msgstr "" @@ -1523,7 +1524,7 @@ msgstr "" msgid "Account: {0} is not permitted under Payment Entry" msgstr "" -#: erpnext/controllers/accounts_controller.py:3261 +#: erpnext/controllers/accounts_controller.py:3263 msgid "Account: {0} with currency: {1} can not be selected" msgstr "" @@ -1796,18 +1797,18 @@ msgstr "" msgid "Accounting Entries" msgstr "" -#: erpnext/assets/doctype/asset/asset.py:858 -#: erpnext/assets/doctype/asset/asset.py:873 -#: erpnext/assets/doctype/asset_capitalization/asset_capitalization.py:561 +#: erpnext/assets/doctype/asset/asset.py:917 +#: erpnext/assets/doctype/asset/asset.py:932 +#: erpnext/assets/doctype/asset_capitalization/asset_capitalization.py:562 msgid "Accounting Entry for Asset" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1882 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1902 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1883 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1903 msgid "Accounting Entry for LCV in Stock Entry {0}" msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:867 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:873 msgid "Accounting Entry for Landed Cost Voucher for SCR {0}" msgstr "" @@ -1827,9 +1828,9 @@ msgstr "" #: erpnext/controllers/stock_controller.py:683 #: erpnext/controllers/stock_controller.py:700 #: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:932 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1827 -#: erpnext/stock/doctype/stock_entry/stock_entry.py:1841 -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:702 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1828 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:1842 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:708 msgid "Accounting Entry for Stock" msgstr "" @@ -1863,7 +1864,7 @@ msgstr "" msgid "Accounting Period" msgstr "" -#: erpnext/accounts/doctype/accounting_period/accounting_period.py:67 +#: erpnext/accounts/doctype/accounting_period/accounting_period.py:68 msgid "Accounting Period overlaps with {0}" msgstr "" @@ -1902,7 +1903,7 @@ msgstr "" #: erpnext/setup/doctype/email_digest/email_digest.json #: erpnext/setup/doctype/incoterm/incoterm.json #: erpnext/setup/doctype/supplier_group/supplier_group.json -#: erpnext/setup/install.py:325 +#: erpnext/setup/install.py:337 msgid "Accounts" msgstr "" @@ -2054,7 +2055,7 @@ msgstr "" #. Label of the accumulated_depreciation_amount (Currency) field in DocType #. 'Depreciation Schedule' #: erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py:178 -#: erpnext/assets/doctype/asset/asset.js:289 +#: erpnext/assets/doctype/asset/asset.js:302 #: erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json msgid "Accumulated Depreciation Amount" msgstr "" @@ -2209,6 +2210,11 @@ msgstr "" msgid "Active Status" msgstr "" +#. Label of a number card in the Subcontracting Workspace +#: erpnext/subcontracting/workspace/subcontracting/subcontracting.json +msgid "Active Subcontracted Items" +msgstr "" + #. Label of the activities_tab (Tab Break) field in DocType 'Lead' #. Label of the activities_tab (Tab Break) field in DocType 'Opportunity' #. Label of the activities_tab (Tab Break) field in DocType 'Prospect' @@ -2297,7 +2303,7 @@ msgstr "" #: erpnext/manufacturing/doctype/job_card/job_card.json #: erpnext/manufacturing/doctype/work_order/work_order.json #: erpnext/manufacturing/report/work_order_summary/work_order_summary.py:254 -#: erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py:115 +#: erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py:129 msgid "Actual End Date" msgstr "" @@ -2430,7 +2436,7 @@ msgstr "" msgid "Actual qty in stock" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1526 +#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1521 #: erpnext/public/js/controllers/accounts.js:197 msgid "Actual type tax cannot be included in Item rate in row {0}" msgstr "" @@ -2616,7 +2622,7 @@ msgid "Add details" msgstr "" #: erpnext/stock/doctype/pick_list/pick_list.js:86 -#: erpnext/stock/doctype/pick_list/pick_list.py:866 +#: erpnext/stock/doctype/pick_list/pick_list.py:882 msgid "Add items in the Item Locations table" msgstr "" @@ -2902,7 +2908,7 @@ msgstr "" msgid "Additional Transferred Qty" msgstr "" -#: erpnext/manufacturing/doctype/work_order/work_order.py:665 +#: erpnext/manufacturing/doctype/work_order/work_order.py:660 msgid "Additional Transferred Qty {0}\n" "\t\t\t\t\tcannot be greater than {1}.\n" "\t\t\t\t\tTo fix this, increase the percentage value\n" @@ -2915,7 +2921,7 @@ msgstr "" msgid "Additional information regarding the customer." msgstr "" -#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:584 +#: erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py:590 msgid "Additional {0} {1} of item {2} required as per BOM to complete this transaction" msgstr "" @@ -3055,7 +3061,7 @@ msgstr "" msgid "Address used to determine Tax Category in transactions" msgstr "" -#: erpnext/assets/doctype/asset/asset.js:146 +#: erpnext/assets/doctype/asset/asset.js:152 msgid "Adjust Asset Value" msgstr "" @@ -3064,7 +3070,7 @@ msgstr "" msgid "Adjust Qty" msgstr "" -#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1112 +#: erpnext/accounts/doctype/sales_invoice/sales_invoice.js:1108 msgid "Adjustment Against" msgstr "" @@ -3247,7 +3253,7 @@ msgstr "" #: erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json #: erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py:39 #: erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py:91 -#: erpnext/accounts/report/general_ledger/general_ledger.py:748 +#: erpnext/accounts/report/general_ledger/general_ledger.py:752 msgid "Against Account" msgstr "" @@ -3365,7 +3371,7 @@ msgstr "" #. Label of the against_voucher (Dynamic Link) field in DocType 'GL Entry' #: erpnext/accounts/doctype/gl_entry/gl_entry.json -#: erpnext/accounts/report/general_ledger/general_ledger.py:781 +#: erpnext/accounts/report/general_ledger/general_ledger.py:785 msgid "Against Voucher" msgstr "" @@ -3389,7 +3395,7 @@ msgstr "" #: erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json #: erpnext/accounts/doctype/gl_entry/gl_entry.json #: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json -#: erpnext/accounts/report/general_ledger/general_ledger.py:779 +#: erpnext/accounts/report/general_ledger/general_ledger.py:783 #: erpnext/accounts/report/payment_ledger/payment_ledger.py:183 msgid "Against Voucher Type" msgstr "" @@ -3527,7 +3533,7 @@ msgstr "" msgid "All Activities HTML" msgstr "" -#: erpnext/manufacturing/doctype/bom/bom.py:306 +#: erpnext/manufacturing/doctype/bom/bom.py:318 msgid "All BOMs" msgstr "" @@ -3667,15 +3673,15 @@ msgstr "" msgid "All items have already been Invoiced/Returned" msgstr "" -#: erpnext/stock/doctype/delivery_note/delivery_note.py:1207 +#: erpnext/stock/doctype/delivery_note/delivery_note.py:1208 msgid "All items have already been received" msgstr "" -#: erpnext/stock/doctype/stock_entry/stock_entry.py:2992 +#: erpnext/stock/doctype/stock_entry/stock_entry.py:2993 msgid "All items have already been transferred for this Work Order." msgstr "" -#: erpnext/public/js/controllers/transaction.js:2907 +#: erpnext/public/js/controllers/transaction.js:2903 msgid "All items in this document already have a linked Quality Inspection." msgstr "" @@ -3701,7 +3707,7 @@ msgstr "" 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." msgstr "" -#: erpnext/stock/doctype/delivery_note/delivery_note.py:857 +#: erpnext/stock/doctype/delivery_note/delivery_note.py:858 msgid "All these items have already been Invoiced/Returned" msgstr "" @@ -3730,7 +3736,7 @@ msgstr "" msgid "Allocate Payment Based On Payment Terms" msgstr "" -#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1716 +#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1711 msgid "Allocate Payment Request" msgstr "" @@ -3761,7 +3767,7 @@ msgstr "" #: erpnext/accounts/doctype/advance_taxes_and_charges/advance_taxes_and_charges.json #: erpnext/accounts/doctype/bank_transaction/bank_transaction.json #: erpnext/accounts/doctype/bank_transaction_payments/bank_transaction_payments.json -#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1707 +#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1702 #: erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json #: erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json #: erpnext/accounts/doctype/purchase_invoice_advance/purchase_invoice_advance.json @@ -4233,7 +4239,7 @@ msgstr "" msgid "Allows users to submit Supplier Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts." msgstr "" -#: erpnext/stock/doctype/pick_list/pick_list.py:1008 +#: erpnext/stock/doctype/pick_list/pick_list.py:1024 msgid "Already Picked" msgstr "" @@ -4269,7 +4275,7 @@ msgstr "" msgid "Alternative Item Name" msgstr "" -#: erpnext/selling/doctype/quotation/quotation.js:362 +#: erpnext/selling/doctype/quotation/quotation.js:369 msgid "Alternative Items" msgstr "" @@ -4448,7 +4454,7 @@ msgstr "" #: erpnext/manufacturing/doctype/bom_item/bom_item.json #: erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json #: erpnext/manufacturing/doctype/work_order_item/work_order_item.json -#: erpnext/selling/doctype/quotation/quotation.js:300 +#: erpnext/selling/doctype/quotation/quotation.js:307 #: erpnext/selling/doctype/quotation_item/quotation_item.json #: erpnext/selling/doctype/sales_order_item/sales_order_item.json #: erpnext/selling/page/point_of_sale/pos_item_cart.js:46 @@ -4481,7 +4487,7 @@ msgstr "" #: erpnext/templates/form_grid/stock_entry_grid.html:11 #: erpnext/templates/pages/order.html:103 erpnext/templates/pages/rfq.html:46 msgid "Amount" -msgstr "" +msgstr "Összeg" #: erpnext/regional/report/uae_vat_201/uae_vat_201.py:22 msgid "Amount (AED)" @@ -4711,7 +4717,7 @@ msgstr "" msgid "Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}" msgstr "" -#: erpnext/accounts/doctype/payment_request/payment_request.py:748 +#: erpnext/accounts/doctype/payment_request/payment_request.py:752 msgid "Another Payment Request is already processed" msgstr "" @@ -5170,7 +5176,7 @@ msgstr "" msgid "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." msgstr "" -#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1794 +#: erpnext/manufacturing/doctype/production_plan/production_plan.py:1802 msgid "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." msgstr "" @@ -5338,7 +5344,7 @@ msgstr "" msgid "Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists." msgstr "" -#: erpnext/assets/doctype/asset/asset.py:181 +#: erpnext/assets/doctype/asset/asset.py:236 msgid "Asset Depreciation Schedules created/updated:Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin