mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-16 16:15:02 +00:00
Merge branch 'develop' into fixed-serial-no-save-performance-issue
This commit is contained in:
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"]},
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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],
|
||||
],
|
||||
|
||||
@@ -11,6 +11,5 @@ def get_data():
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
|
||||
{"items": ["Budget"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"]],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -44,3 +44,5 @@ frappe.query_reports[CF_REPORT_NAME]["filters"].push(
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
frappe.query_reports[CF_REPORT_NAME]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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 & 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 & 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"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 & 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"
|
||||
}
|
||||
@@ -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 & 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"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 & 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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 & 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 & 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"
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 & 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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user