Merge branch 'develop' into fixed-serial-no-save-performance-issue

This commit is contained in:
rohitwaghchaure
2026-01-09 16:57:24 +05:30
committed by GitHub
300 changed files with 27114 additions and 21626 deletions

View File

@@ -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": []
}
}

View File

@@ -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": []
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -125,7 +125,7 @@ class BankClearance(Document):
)
msg += "</ul>"
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"))

View File

@@ -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": []
}
}

View File

@@ -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:

View File

@@ -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()

View File

@@ -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",

View File

@@ -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),

View File

@@ -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"]},

View File

@@ -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):

View File

@@ -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"];
}
},
};

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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],
],

View File

@@ -11,6 +11,5 @@ def get_data():
},
"transactions": [
{"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
{"items": ["Budget"]},
],
}

View File

@@ -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;
}
});

View File

@@ -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:

View File

@@ -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 = (

View File

@@ -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",

View File

@@ -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

View File

@@ -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(

View File

@@ -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()

View File

@@ -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"):

View File

@@ -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"]],
],
};
});

View File

@@ -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",

View File

@@ -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);
},
});

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"

View File

@@ -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);
},

View File

@@ -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 = {

View File

@@ -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:

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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})

View File

@@ -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
}
}

View File

@@ -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 = []

View File

@@ -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):

View File

@@ -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):

View File

@@ -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,
)
)

View File

@@ -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);
}
},
};

View File

@@ -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);
}
},
};

View File

@@ -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);
}
},
};

View File

@@ -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);
}
},
};

View File

@@ -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"
}
]
}
],
"timeout": 0
}

View File

@@ -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",

View File

@@ -44,3 +44,5 @@ frappe.query_reports[CF_REPORT_NAME]["filters"].push(
fieldtype: "Check",
}
);
frappe.query_reports[CF_REPORT_NAME]["export_hidden_cols"] = true;

View File

@@ -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()

View File

@@ -13,7 +13,7 @@
}
.financial-statements-blank-row td {
height: 37px;
height: 20px;
}
</style>
@@ -25,30 +25,37 @@
{% endif %}
<h3 class="text-center">{%= filters.fiscal_year %}</h3>
<h5 class="text-center">
{%= __("Currency") %} : {%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
<strong>{%= __("Currency") %}:</strong> {%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</h5>
{% if (filters.from_date) { %}
<h5 class="text-center">
{%= frappe.datetime.str_to_user(filters.from_date) %} - {%= frappe.datetime.str_to_user(filters.to_date) %}
</h5>
{% } %}
<hr>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
{% if subtitle %}
<div class="show-filters">
{{ subtitle }}
<hr>
</div>
{% endif %}
<table class="table table-bordered">
<thead>
<tr>
<th style="width: {%= 100 - (report_columns.length - 1) * 13 %}%"></th>
<th style="width: {%= 100 - (report_columns.length - 1) * 16 %}%">{%= report_columns[0].label %}</th>
{% for (let i=1, l=report_columns.length; i<l; i++) { %}
<th class="text-right">{%= report_columns[i].label %}</th>
{% } %}
</tr>
</thead>
<tbody>
{% for(let j=0, k=data.length; j<k; j++) { %}
{%
@@ -60,6 +67,7 @@
<td>
<span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name || row.section %}</span>
</td>
{% for(let i=1, l=report_columns.length; i<l; i++) { %}
<td class="text-right">
{% const fieldname = report_columns[i].fieldname; %}
@@ -72,6 +80,7 @@
{% } %}
</tbody>
</table>
<p class="text-right text-muted">
{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}
</p>

View File

@@ -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,
},
],
};

View File

@@ -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)

View File

@@ -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
)
)

View File

@@ -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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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"
}
],

View File

@@ -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"
}
"title": "Financial Reports",
"type": "Workspace"
}

View File

@@ -1,204 +0,0 @@
{
"charts": [],
"content": "[{\"id\":\"rMMsfn2eB4\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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"
}

View File

@@ -1,254 +0,0 @@
{
"charts": [],
"content": "[{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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"
}

View File

@@ -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,

View File

@@ -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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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"
}
"shortcuts": [],
"title": "Assets",
"type": "Workspace"
}

View File

@@ -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",

View File

@@ -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()

View File

@@ -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"),

View File

@@ -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": []
}
}

View File

@@ -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(

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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})

View File

@@ -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,
},
]

View File

@@ -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 = "<span style='color:green'>" + value + "</span>";
}
return value;

View File

@@ -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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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"
}

View File

@@ -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):

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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()

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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 (

View File

@@ -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": [
{

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"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 <a href=\\\"https://frappe.io/crm\\\">Frappe CRM</a> 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"
}
"shortcuts": [],
"title": "CRM",
"type": "Workspace"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

Some files were not shown because too many files have changed in this diff Show More