mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-14 09:23:09 +00:00
Merge branch 'develop' into standard-letter-heads
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2018-12-28 22:29:21.828090",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 15,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "fax",
|
||||
"label": "Tax Category",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2018-12-28 22:29:21.828090",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-tax_category",
|
||||
"no_copy": 0,
|
||||
"options": "Tax Category",
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2020-10-14 17:41:40.878179",
|
||||
"default": "0",
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_your_company_address",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 20,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "linked_with",
|
||||
"label": "Is Your Company Address",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2020-10-14 17:41:40.878179",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-is_your_company_address",
|
||||
"no_copy": 0,
|
||||
"options": null,
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Address",
|
||||
"property_setters": [],
|
||||
"sync_on_migrate": 1
|
||||
}
|
||||
@@ -126,7 +126,7 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nStock Delivered But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -65,6 +65,7 @@ class Account(NestedSet):
|
||||
"Stock",
|
||||
"Stock Adjustment",
|
||||
"Stock Received But Not Billed",
|
||||
"Stock Delivered But Not Billed",
|
||||
"Service Received But Not Billed",
|
||||
"Tax",
|
||||
"Temporary",
|
||||
@@ -174,16 +175,19 @@ class Account(NestedSet):
|
||||
if cint(self.is_group):
|
||||
db_value = self.get_doc_before_save()
|
||||
if db_value:
|
||||
Account = frappe.qb.DocType("Account")
|
||||
query = frappe.qb.update(Account).where((Account.lft > self.lft) & (Account.rgt < self.rgt))
|
||||
|
||||
updated = False
|
||||
if self.report_type != db_value.report_type:
|
||||
frappe.db.sql(
|
||||
"update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
|
||||
(self.report_type, self.lft, self.rgt),
|
||||
)
|
||||
query = query.set(Account.report_type, self.report_type)
|
||||
updated = True
|
||||
if self.root_type != db_value.root_type:
|
||||
frappe.db.sql(
|
||||
"update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
|
||||
(self.root_type, self.lft, self.rgt),
|
||||
)
|
||||
query = query.set(Account.root_type, self.root_type)
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
query.run()
|
||||
|
||||
if self.root_type and not self.report_type:
|
||||
self.report_type = (
|
||||
@@ -448,11 +452,7 @@ class Account(NestedSet):
|
||||
return frappe.db.get_value("GL Entry", {"account": self.name})
|
||||
|
||||
def check_if_child_exists(self):
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabAccount` where parent_account = %s
|
||||
and docstatus != 2""",
|
||||
self.name,
|
||||
)
|
||||
return frappe.db.exists("Account", {"parent_account": self.name, "docstatus": ["!=", 2]})
|
||||
|
||||
def validate_mandatory(self):
|
||||
if not self.root_type:
|
||||
@@ -472,14 +472,24 @@ class Account(NestedSet):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_parent_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||
return frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where is_group = 1 and docstatus != 2 and company = {}
|
||||
and {} like {} order by name limit {} offset {}""".format("%s", searchfield, "%s", "%s", "%s"),
|
||||
(filters["company"], "%%%s%%" % txt, page_len, start),
|
||||
as_list=1,
|
||||
Account = frappe.qb.DocType("Account")
|
||||
|
||||
search_field_obj = getattr(Account, searchfield)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Account)
|
||||
.select(Account.name)
|
||||
.where(Account.is_group == 1)
|
||||
.where(Account.docstatus != 2)
|
||||
.where(Account.company == filters["company"])
|
||||
.where(search_field_obj.like(f"%{txt}%"))
|
||||
.order_by(Account.name)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
return query.run(as_list=1)
|
||||
|
||||
|
||||
def get_account_currency(account):
|
||||
"""Helper function to get account currency"""
|
||||
@@ -520,6 +530,7 @@ def update_account_number(
|
||||
):
|
||||
_ensure_idle_system()
|
||||
account = frappe.get_cached_doc("Account", name)
|
||||
account.check_permission("write")
|
||||
if not account:
|
||||
return
|
||||
|
||||
@@ -673,6 +684,7 @@ def get_company_default_account_fields():
|
||||
"default_expense_account": "Default Expense Account",
|
||||
"default_income_account": "Default Income Account",
|
||||
"stock_received_but_not_billed": "Stock Received But Not Billed Account",
|
||||
"stock_delivered_but_not_billed": "Stock Delivered But Not Billed Account",
|
||||
"stock_adjustment_account": "Stock Adjustment Account",
|
||||
"write_off_account": "Write Off Account",
|
||||
"default_discount_account": "Default Payment Discount Account",
|
||||
|
||||
@@ -378,6 +378,9 @@
|
||||
"Passifs de stock": {
|
||||
"Stock re\u00e7u non factur\u00e9": {
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Stock livr\u00e9 non factur\u00e9": {
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
}
|
||||
},
|
||||
"Provision pour vacances et cong\u00e9s": {},
|
||||
|
||||
@@ -221,6 +221,10 @@
|
||||
"account_number": "1702",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Warenausgangs-Verrechnungskonto": {
|
||||
"account_number": "1703",
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
},
|
||||
"Verbindlichkeiten aus Lohn und Gehalt": {
|
||||
"account_number": "1740",
|
||||
"account_type": "Payable"
|
||||
|
||||
@@ -1144,6 +1144,10 @@
|
||||
"Wareneingangs-Verrechnungskonto" : {
|
||||
"account_number": "70001",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Warenausgangs-Verrechnungskonto" : {
|
||||
"account_number": "70002",
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
}
|
||||
},
|
||||
"Verb. aus Lieferungen und Leistungen": {
|
||||
|
||||
@@ -1076,6 +1076,9 @@
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_number": "4088"
|
||||
}
|
||||
},
|
||||
"Stock livr\u00e9 non factur\u00e9": {
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
}
|
||||
},
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
|
||||
@@ -1589,6 +1589,9 @@
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_number": "4088"
|
||||
}
|
||||
},
|
||||
"Stock livr\u00e9 non factur\u00e9": {
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
}
|
||||
},
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
|
||||
@@ -1592,6 +1592,9 @@
|
||||
"account_number": "4088"
|
||||
},
|
||||
"account_number": "408"
|
||||
},
|
||||
"Stock livr\u00e9 non factur\u00e9": {
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
}
|
||||
},
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
|
||||
@@ -805,6 +805,9 @@
|
||||
},
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Stock livr\u00e9 non factur\u00e9": {
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
},
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
|
||||
@@ -1520,6 +1520,9 @@
|
||||
"account_number": "4088"
|
||||
},
|
||||
"account_number": "408"
|
||||
},
|
||||
"Stock livr\u00e9 non factur\u00e9": {
|
||||
"account_type": "Stock Delivered But Not Billed"
|
||||
}
|
||||
},
|
||||
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
|
||||
|
||||
@@ -223,6 +223,10 @@
|
||||
"Stock Received But Not Billed": {
|
||||
"account_type": "Stock Received But Not Billed",
|
||||
"account_category": "Trade Payables"
|
||||
},
|
||||
"Stock Delivered But Not Billed": {
|
||||
"account_type": "Stock Delivered But Not Billed",
|
||||
"account_category": "Trade Payables"
|
||||
}
|
||||
},
|
||||
"Duties and Taxes": {
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
{
|
||||
"country_code": "nz",
|
||||
"name": "New Zealand - Chart of Accounts with Account Numbers",
|
||||
"disabled": "No",
|
||||
"tree": {
|
||||
"Application of Funds (Assets)": {
|
||||
"Current Assets": {
|
||||
"Bank Accounts": {
|
||||
"Business Transaction Account": {
|
||||
"account_number": "11011",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Business Savings Account": {
|
||||
"account_number": "11012",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"account_number": "11010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Cash on Hand": {
|
||||
"account_number": "11020",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Accounts Receivable": {
|
||||
"Debtors": {
|
||||
"account_number": "11210",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Provision for Doubtful Debts": {
|
||||
"account_number": "11220"
|
||||
},
|
||||
"account_number": "11200",
|
||||
"is_group": 1
|
||||
},
|
||||
"Inventory": {
|
||||
"Stock on Hand": {
|
||||
"account_number": "11311",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Work In Progress": {
|
||||
"account_number": "11312",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"account_number": "11310",
|
||||
"account_type": "Stock",
|
||||
"is_group": 1
|
||||
},
|
||||
"Prepayments": {
|
||||
"Prepayments": {
|
||||
"account_number": "11411"
|
||||
},
|
||||
"Supplier Advances": {
|
||||
"account_number": "11412"
|
||||
},
|
||||
"Deferred Expense": {
|
||||
"account_number": "11413"
|
||||
},
|
||||
"account_number": "11410",
|
||||
"is_group": 1
|
||||
},
|
||||
"GST Receivable": {
|
||||
"account_number": "11510",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Income Tax Receivable": {
|
||||
"account_number": "11520",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "11000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Fixed Assets": {
|
||||
"Plant & Equipment": {
|
||||
"Plant & Equipment": {
|
||||
"account_number": "16011",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Plant & Equipment": {
|
||||
"account_number": "16012",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Motor Vehicles": {
|
||||
"Motor Vehicles": {
|
||||
"account_number": "16021",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Motor Vehicles": {
|
||||
"account_number": "16022",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16020",
|
||||
"is_group": 1
|
||||
},
|
||||
"Office Equipment": {
|
||||
"Office Equipment": {
|
||||
"account_number": "16031",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Office Equipment": {
|
||||
"account_number": "16032",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16030",
|
||||
"is_group": 1
|
||||
},
|
||||
"Buildings": {
|
||||
"Buildings": {
|
||||
"account_number": "16041",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Buildings": {
|
||||
"account_number": "16042",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16040",
|
||||
"is_group": 1
|
||||
},
|
||||
"Computer Equipment": {
|
||||
"Computer Equipment": {
|
||||
"account_number": "16051",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Computer Equipment": {
|
||||
"account_number": "16052",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16050",
|
||||
"is_group": 1
|
||||
},
|
||||
"Capital Work in Progress": {
|
||||
"account_number": "16090",
|
||||
"account_type": "Capital Work in Progress"
|
||||
},
|
||||
"account_number": "16000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "10000",
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Source of Funds (Liabilities)": {
|
||||
"Current Liabilities": {
|
||||
"Accounts Payable": {
|
||||
"Creditors": {
|
||||
"account_number": "21010",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_number": "21000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Goods Received Not Invoiced": {
|
||||
"account_number": "21100",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Asset Received Not Invoiced": {
|
||||
"account_number": "21110",
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
},
|
||||
"Service Received Not Invoiced": {
|
||||
"account_number": "21120",
|
||||
"account_type": "Service Received But Not Billed"
|
||||
},
|
||||
"Accrued Expenses": {
|
||||
"account_number": "21200"
|
||||
},
|
||||
"Wages Payable": {
|
||||
"account_number": "21300"
|
||||
},
|
||||
"PAYE Payable": {
|
||||
"account_number": "22010"
|
||||
},
|
||||
"KiwiSaver Payable": {
|
||||
"account_number": "22020"
|
||||
},
|
||||
"ACC Payable": {
|
||||
"account_number": "22030"
|
||||
},
|
||||
"Credit Cards": {
|
||||
"Business Credit Card": {
|
||||
"account_number": "22110"
|
||||
},
|
||||
"account_number": "22100",
|
||||
"is_group": 1
|
||||
},
|
||||
"Customer Advances": {
|
||||
"account_number": "22200"
|
||||
},
|
||||
"Deferred Revenue": {
|
||||
"account_number": "22210"
|
||||
},
|
||||
"Provisional Account": {
|
||||
"account_number": "22220"
|
||||
},
|
||||
"Tax Liabilities": {
|
||||
"GST Payable": {
|
||||
"account_number": "22310",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"GST Suspense": {
|
||||
"account_number": "22320",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"FBT Payable": {
|
||||
"account_number": "22330",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Income Tax Payable": {
|
||||
"account_number": "22340",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "22300",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "21500",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non-Current Liabilities": {
|
||||
"Bank Loans": {
|
||||
"Bank Loan": {
|
||||
"account_number": "25011"
|
||||
},
|
||||
"account_number": "25010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities": {
|
||||
"Lease Liability": {
|
||||
"account_number": "25021"
|
||||
},
|
||||
"account_number": "25020",
|
||||
"is_group": 1
|
||||
},
|
||||
"Shareholder Loans": {
|
||||
"Shareholder Loan": {
|
||||
"account_number": "25031"
|
||||
},
|
||||
"account_number": "25030",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "25000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "20000",
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Equity": {
|
||||
"Share Capital": {
|
||||
"account_number": "31010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Drawings": {
|
||||
"account_number": "31020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Current Year Earnings": {
|
||||
"account_number": "35010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "35020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "30000",
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Income": {
|
||||
"Sales": {
|
||||
"account_number": "41010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Other Income": {
|
||||
"Interest Income": {
|
||||
"account_number": "47010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Rounding Gain/Loss": {
|
||||
"account_number": "47020",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Foreign Exchange Gain": {
|
||||
"account_number": "47030",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"account_number": "47000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "40000",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Expenses": {
|
||||
"Cost of Goods Sold": {
|
||||
"Purchases": {
|
||||
"account_number": "51010",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Freight Inwards": {
|
||||
"account_number": "51020",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Duty and Landing Costs": {
|
||||
"account_number": "51030",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "51040",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Write Off": {
|
||||
"account_number": "51050",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"account_number": "51000",
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"is_group": 1
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"Wages & Salaries": {
|
||||
"account_number": "61010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"KiwiSaver Employer Contribution": {
|
||||
"account_number": "61020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"ACC Levies": {
|
||||
"account_number": "61030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Rent": {
|
||||
"account_number": "65010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Power": {
|
||||
"account_number": "65020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Telephone": {
|
||||
"account_number": "66010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Insurance": {
|
||||
"account_number": "64010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Accounting Fees": {
|
||||
"account_number": "64020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Legal Fees": {
|
||||
"account_number": "64030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Advertising and Marketing": {
|
||||
"account_number": "65030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Repairs and Maintenance": {
|
||||
"account_number": "65040",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Freight and Courier": {
|
||||
"account_number": "65050",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Operating Costs": {
|
||||
"account_number": "65060",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "60000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Depreciation and Amortisation": {
|
||||
"Depreciation - Plant & Equipment": {
|
||||
"account_number": "62010",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Motor Vehicles": {
|
||||
"account_number": "62020",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Office Equipment": {
|
||||
"account_number": "62030",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Computer Equipment": {
|
||||
"account_number": "62040",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"account_number": "62000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Finance Costs": {
|
||||
"Bank Charges": {
|
||||
"account_number": "67010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Interest Expense": {
|
||||
"account_number": "67020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Rounding Off": {
|
||||
"account_number": "67030",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Payment Discounts": {
|
||||
"account_number": "67040",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "67000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Income Tax Expense": {
|
||||
"account_number": "81010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Foreign Exchange": {
|
||||
"Exchange Gain/Loss": {
|
||||
"account_number": "82010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Unrealized Exchange Gain/Loss": {
|
||||
"account_number": "82020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "82000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bad Debts": {
|
||||
"account_number": "83010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Write Off": {
|
||||
"account_number": "83020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Gain/Loss on Asset Disposal": {
|
||||
"account_number": "83030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Expenses Included In Asset Valuation": {
|
||||
"account_number": "84010",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"account_number": "50000",
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -570,6 +570,17 @@
|
||||
"account_number": "5000",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Cost of Goods Sold": {
|
||||
"account_number": "5001",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Cost of Goods Sold": {
|
||||
"account_number": "5010",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
}
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"account_number": "5100",
|
||||
"is_group": 1,
|
||||
|
||||
@@ -35,6 +35,10 @@ def get():
|
||||
_("Short-term Investments"): {"account_category": "Short-term Investments"},
|
||||
_("Stock Assets"): {
|
||||
_("Stock In Hand"): {"account_type": "Stock", "account_category": "Stock Assets"},
|
||||
_("Stock Delivered But Not Billed"): {
|
||||
"account_type": "Stock Delivered But Not Billed",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
"account_type": "Stock",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
|
||||
@@ -62,6 +62,11 @@ def get():
|
||||
"account_number": "1410",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
_("Stock Delivered But Not Billed"): {
|
||||
"account_type": "Stock Delivered But Not Billed",
|
||||
"account_number": "1420",
|
||||
"account_category": "Stock Assets",
|
||||
},
|
||||
"account_type": "Stock",
|
||||
"account_number": "1400",
|
||||
"account_category": "Stock Assets",
|
||||
|
||||
@@ -43,6 +43,7 @@ class AccountingDimension(Document):
|
||||
def validate(self):
|
||||
self.validate_doctype()
|
||||
validate_column_name(self.fieldname)
|
||||
self.validate_fieldname_conflict()
|
||||
self.validate_dimension_defaults()
|
||||
|
||||
def validate_doctype(self):
|
||||
@@ -74,6 +75,27 @@ class AccountingDimension(Document):
|
||||
message += _("Please create a new Accounting Dimension if required.")
|
||||
frappe.throw(message)
|
||||
|
||||
def validate_fieldname_conflict(self):
|
||||
conflicting_doctypes = []
|
||||
for doctype in get_doctypes_with_dimensions():
|
||||
meta = frappe.get_meta(doctype, cached=False)
|
||||
if any(f.fieldname == self.fieldname for f in meta.get("fields")):
|
||||
conflicting_doctypes.append(doctype)
|
||||
|
||||
if conflicting_doctypes:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Fieldname {0} already exists in the following doctypes: {1}. "
|
||||
"A separate dimension field will not be added to these doctypes. "
|
||||
"GL Entries will use the value of the existing field as the dimension value."
|
||||
).format(
|
||||
frappe.bold(self.fieldname),
|
||||
", ".join(frappe.bold(d) for d in conflicting_doctypes),
|
||||
),
|
||||
title=_("Fieldname Conflict"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
def validate_dimension_defaults(self):
|
||||
companies = []
|
||||
for default in self.get("dimension_defaults"):
|
||||
|
||||
@@ -38,16 +38,6 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
|
||||
@@ -97,7 +97,6 @@
|
||||
"receivable_payable_fetch_method",
|
||||
"default_ageing_range",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"tab_break_dpet",
|
||||
@@ -526,7 +525,7 @@
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
@@ -595,13 +594,6 @@
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
@@ -749,7 +741,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-22 01:38:42.418238",
|
||||
"modified": "2026-05-18 12:16:33.679345",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -91,7 +91,7 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
preview_mode: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
repost_allowed_types: DF.Table[RepostAllowedTypes]
|
||||
@@ -212,13 +212,6 @@ class AccountsSettings(Document):
|
||||
|
||||
set_allow_on_submit_for_dimension_fields(doctypes)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
|
||||
def toggle_accounting_dimension_sections(hide):
|
||||
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "format:Bank Statement Import on {creation}",
|
||||
"beta": 1,
|
||||
"creation": "2019-08-04 14:16:08.318714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -226,11 +226,11 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-11 02:23:22.159961",
|
||||
"modified": "2026-05-31 00:41:11.251215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
"naming_rule": "Expression",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
from_date=bank_transaction.date,
|
||||
to_date=utils.today(),
|
||||
)
|
||||
self.assertTrue(linked_payments[0]["party"] == "Conrad Electronic")
|
||||
self.assertEqual(linked_payments[0]["party"], "Conrad Electronic")
|
||||
|
||||
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
||||
def test_reconcile(self):
|
||||
@@ -70,10 +70,10 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
unallocated_amount = frappe.db.get_value(
|
||||
"Bank Transaction", bank_transaction.name, "unallocated_amount"
|
||||
)
|
||||
self.assertTrue(unallocated_amount == 0)
|
||||
self.assertEqual(unallocated_amount, 0)
|
||||
|
||||
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
|
||||
self.assertTrue(clearance_date is not None)
|
||||
self.assertIsNot(clearance_date, None)
|
||||
|
||||
bank_transaction.reload()
|
||||
bank_transaction.cancel()
|
||||
@@ -178,9 +178,8 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0
|
||||
)
|
||||
self.assertTrue(
|
||||
frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date")
|
||||
is not None
|
||||
self.assertIsNot(
|
||||
frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date"), None
|
||||
)
|
||||
|
||||
@if_lending_app_installed
|
||||
|
||||
@@ -705,18 +705,20 @@ def get_ordered_amount(params):
|
||||
|
||||
|
||||
def get_other_condition(params, for_doc):
|
||||
condition = f"expense_account = '{params.expense_account}'"
|
||||
condition = f"expense_account = {frappe.db.escape(params.expense_account)}"
|
||||
budget_against_field = params.get("budget_against_field")
|
||||
|
||||
if budget_against_field and params.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
|
||||
condition += (
|
||||
f" and child.{budget_against_field} = {frappe.db.escape(params.get(budget_against_field))}"
|
||||
)
|
||||
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
|
||||
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
|
||||
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
|
||||
|
||||
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
|
||||
condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ class TestCostCenterAllocation(ERPNextTestSuite):
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertTrue(gle.cost_center in expected_values)
|
||||
self.assertIn(gle.cost_center, expected_values)
|
||||
self.assertEqual(gle.debit, 0)
|
||||
self.assertEqual(gle.credit, expected_values[gle.cost_center])
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "naming_series:",
|
||||
"beta": 1,
|
||||
"creation": "2019-07-05 16:34:31.013238",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
@@ -400,7 +400,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-26 13:46:07.760867",
|
||||
"modified": "2026-05-30 23:18:04.712528",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
@@ -449,9 +449,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_rename": 1,
|
||||
"beta": 1,
|
||||
"creation": "2019-12-04 04:59:08.003664",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -107,7 +107,7 @@
|
||||
"link_fieldname": "dunning_type"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-27 13:08:19.584112",
|
||||
"modified": "2026-05-30 23:18:20.740726",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Type",
|
||||
@@ -151,8 +151,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,18 +565,19 @@ class FinancialQueryBuilder:
|
||||
frappe.qb.from_(acb_table)
|
||||
.select(
|
||||
acb_table.account,
|
||||
(acb_table.debit - acb_table.credit).as_("balance"),
|
||||
Sum(acb_table.debit - acb_table.credit).as_("balance"),
|
||||
)
|
||||
.where(acb_table.company == self.company)
|
||||
.where(acb_table.account.isin(account_names))
|
||||
.where(acb_table.period_closing_voucher == closing_voucher)
|
||||
.groupby(acb_table.account)
|
||||
)
|
||||
|
||||
query = self._apply_standard_filters(query, acb_table, "Account Closing Balance")
|
||||
results = self._execute_with_permissions(query, "Account Closing Balance")
|
||||
|
||||
for row in results:
|
||||
closing_balances[row["account"]] = row["balance"]
|
||||
closing_balances[row["account"]] = row["balance"] or 0.0
|
||||
|
||||
return closing_balances
|
||||
|
||||
|
||||
@@ -361,7 +361,7 @@ class CalculationFormulaValidator(Validator):
|
||||
"sqrt": lambda x: x**0.5,
|
||||
"pow": pow,
|
||||
"ceil": lambda x: int(x) + (1 if x % 1 else 0),
|
||||
"floor": lambda x: int(x),
|
||||
"floor": int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.financial_report_template.test_financial_report_te
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_currency_precision, get_fiscal_year
|
||||
from erpnext.tests.utils import change_settings
|
||||
|
||||
|
||||
class TestDependencyResolver(FinancialReportTemplateTestCase):
|
||||
@@ -1950,6 +1951,104 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
|
||||
|
||||
jv_2023.cancel()
|
||||
|
||||
@change_settings("Accounts Settings", {"use_legacy_controller_for_pcv": 1})
|
||||
def test_opening_balance_sums_acb_rows_across_dimensions(self):
|
||||
"""
|
||||
Account Closing Balance stores one row per (account, cost_center,
|
||||
project, finance_book). The closing-balance fetch must sum all rows.
|
||||
"""
|
||||
company = "_Test Company"
|
||||
cash_account = "_Test Cash - _TC"
|
||||
sales_account = "Sales - _TC"
|
||||
cc_1 = "_Test Cost Center - _TC"
|
||||
cc_2 = "_Test Cost Center 2 - _TC"
|
||||
docs = []
|
||||
|
||||
try:
|
||||
jv_2023_cc1 = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=sales_account,
|
||||
amount=3000,
|
||||
posting_date="2023-06-15",
|
||||
cost_center=cc_1,
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
docs.append(jv_2023_cc1)
|
||||
jv_2023_cc2 = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=sales_account,
|
||||
amount=2000,
|
||||
posting_date="2023-06-15",
|
||||
cost_center=cc_2,
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
docs.append(jv_2023_cc2)
|
||||
|
||||
fy_2023 = get_fiscal_year("2023-06-15", company=company)
|
||||
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": "2023-12-31",
|
||||
"period_start_date": fy_2023[1],
|
||||
"period_end_date": fy_2023[2],
|
||||
"company": company,
|
||||
"fiscal_year": fy_2023[0],
|
||||
"cost_center": cc_1,
|
||||
"closing_account_head": "Deferred Revenue - _TC",
|
||||
"remarks": "Test multi-dim PCV",
|
||||
}
|
||||
)
|
||||
pcv.insert()
|
||||
pcv.submit()
|
||||
docs.append(pcv)
|
||||
|
||||
jv_2024 = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=sales_account,
|
||||
amount=100,
|
||||
posting_date="2024-01-15",
|
||||
cost_center=cc_1,
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
docs.append(jv_2024)
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-01-01",
|
||||
"period_end_date": "2024-03-31",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Monthly",
|
||||
"ignore_closing_entries": True,
|
||||
}
|
||||
periods = [
|
||||
{"key": "2024_jan", "from_date": "2024-01-01", "to_date": "2024-01-31"},
|
||||
{"key": "2024_feb", "from_date": "2024-02-01", "to_date": "2024-02-29"},
|
||||
{"key": "2024_mar", "from_date": "2024-03-01", "to_date": "2024-03-31"},
|
||||
]
|
||||
|
||||
query_builder = FinancialQueryBuilder(filters, periods)
|
||||
accounts = [
|
||||
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||
]
|
||||
|
||||
balances_data = query_builder.fetch_account_balances(accounts)
|
||||
cash_data = balances_data.get(cash_account)
|
||||
self.assertIsNotNone(cash_data, "Cash account must appear in results")
|
||||
|
||||
jan_cash = cash_data.get_period("2024_jan")
|
||||
self.assertEqual(jan_cash.opening, 5000.0)
|
||||
self.assertEqual(jan_cash.movement, 100.0)
|
||||
self.assertEqual(jan_cash.closing, 5100.0)
|
||||
|
||||
finally:
|
||||
self.cancel_docs(docs)
|
||||
|
||||
def test_opening_entries_roll_into_opening_after_period_closing(self):
|
||||
"""
|
||||
Sequence:
|
||||
|
||||
@@ -9,6 +9,14 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
class FinancialReportTemplateTestCase(ERPNextTestSuite):
|
||||
"""Utility class with common setup and helper methods for all test classes"""
|
||||
|
||||
def cancel_docs(self, docs):
|
||||
"""Cancel submitted docs in reverse creation order to avoid dependency issues."""
|
||||
for doc in reversed(docs):
|
||||
if doc:
|
||||
doc.reload()
|
||||
if doc.docstatus == 1:
|
||||
doc.cancel()
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data"""
|
||||
self.create_test_template()
|
||||
|
||||
@@ -433,15 +433,17 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
|
||||
accounts_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
row.exchange_rate = 1;
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
if (!row.exchange_rate) row.exchange_rate = 1;
|
||||
if (!row.account) {
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// set difference
|
||||
if (doc.difference) {
|
||||
|
||||
@@ -1292,7 +1292,11 @@ class JournalEntry(AccountsController):
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def get_values(self):
|
||||
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
|
||||
cond = (
|
||||
f" and outstanding_amount <= {flt(self.write_off_amount)}"
|
||||
if flt(self.write_off_amount) > 0
|
||||
else ""
|
||||
)
|
||||
|
||||
if self.write_off_based_on == "Accounts Receivable":
|
||||
return frappe.db.sql(
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
)
|
||||
payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr)
|
||||
|
||||
self.assertTrue(flt(advance_paid[0][0]) == flt(payment_against_order))
|
||||
self.assertEqual(flt(advance_paid[0][0]), flt(payment_against_order))
|
||||
|
||||
def cancel_against_voucher_testcase(self, test_voucher):
|
||||
if test_voucher.doctype == "Journal Entry":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_copy": 1,
|
||||
"beta": 1,
|
||||
"creation": "2017-08-29 02:22:54.947711",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -90,7 +90,7 @@
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-31 01:47:20.360352",
|
||||
"modified": "2026-05-30 23:18:48.691227",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
{
|
||||
"fieldname": "advance_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Advance Account",
|
||||
"options": "Account"
|
||||
}
|
||||
@@ -36,14 +37,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:08.489183",
|
||||
"modified": "2026-05-27 14:19:00.888437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Party Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,31 +710,12 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (!frm.doc.paid_from_account_currency || !frm.doc.company) return;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
from_currency: frm.doc.paid_from_account_currency,
|
||||
to_currency: company_currency,
|
||||
transaction_date: frm.doc.posting_date,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
frm.set_value("source_exchange_rate", r.message);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
frm.events.set_current_exchange_rate(
|
||||
frm,
|
||||
"source_exchange_rate",
|
||||
frm.doc.paid_from_account_currency,
|
||||
company_currency
|
||||
);
|
||||
}
|
||||
}
|
||||
frm.events.set_current_exchange_rate(
|
||||
frm,
|
||||
"source_exchange_rate",
|
||||
frm.doc.paid_from_account_currency,
|
||||
company_currency
|
||||
);
|
||||
},
|
||||
|
||||
paid_to_account_currency: function (frm) {
|
||||
@@ -766,49 +747,24 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
posting_date: function (frm) {
|
||||
frm.events.paid_from_account_currency(frm);
|
||||
frm.events.paid_to_account_currency(frm);
|
||||
},
|
||||
|
||||
source_exchange_rate: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (frm.doc.paid_amount) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
// no need trigger separately
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
}
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
|
||||
target_exchange_rate: function (frm) {
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.received_amount) {
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
if (frm.doc.base_received_amount && frm.doc.source_exchange_rate) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
|
||||
if (
|
||||
!frm.doc.source_exchange_rate &&
|
||||
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
|
||||
) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
@@ -817,6 +773,32 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
frm.set_paid_amount_based_on_received_amount = false;
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
|
||||
target_exchange_rate: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.base_paid_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (
|
||||
!frm.doc.source_exchange_rate &&
|
||||
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
|
||||
) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
} else {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
// no need trigger separately
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
}
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("target_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
@@ -825,11 +807,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
} else if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
}
|
||||
}
|
||||
frm.trigger("reset_received_amount");
|
||||
@@ -846,15 +831,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (frm.doc.source_exchange_rate) {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1742,6 +1726,35 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
before_cancel: function (frm) {
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_linked_bank_transactions",
|
||||
args: { payment_entry: frm.doc.name },
|
||||
callback: function (r) {
|
||||
const linked = r.message || [];
|
||||
if (!linked.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const bt_links = linked
|
||||
.map((name) => frappe.utils.get_form_link("Bank Transaction", name, true))
|
||||
.join(", ");
|
||||
frappe.confirm(
|
||||
__(
|
||||
"This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?",
|
||||
[bt_links]
|
||||
),
|
||||
() => resolve(),
|
||||
() => reject(),
|
||||
__("Yes"),
|
||||
__("No")
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Reference", {
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.received_amount",
|
||||
"depends_on": "eval:doc.received_amount;",
|
||||
"fieldname": "base_received_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Received Amount (Company Currency)",
|
||||
@@ -795,7 +795,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2026-03-09 17:15:30.453920",
|
||||
"modified": "2026-05-15 13:31:01.166010",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -1238,9 +1238,9 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
remarks = [
|
||||
_("Amount {0} {1} {2} {3}").format(
|
||||
_(self.paid_to_account_currency)
|
||||
_(self.paid_from_account_currency)
|
||||
if self.payment_type == "Receive"
|
||||
else _(self.paid_from_account_currency),
|
||||
else _(self.paid_to_account_currency),
|
||||
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
|
||||
_("received from") if self.payment_type == "Receive" else _("paid to"),
|
||||
self.party,
|
||||
@@ -1256,7 +1256,7 @@ class PaymentEntry(AccountsController):
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} against {2} {3}").format(
|
||||
_("Amount {0} {1} adjusted against {2} {3}").format(
|
||||
_(self.party_account_currency),
|
||||
d.allocated_amount,
|
||||
d.reference_doctype,
|
||||
@@ -1267,7 +1267,7 @@ class PaymentEntry(AccountsController):
|
||||
for d in self.get("deductions"):
|
||||
if d.amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} deducted against {2}").format(
|
||||
_("Amount {0} {1} as adjustment to {2}").format(
|
||||
_(self.company_currency), d.amount, d.account
|
||||
)
|
||||
)
|
||||
@@ -3574,3 +3574,16 @@ def make_payment_order(source_name: str, target_doc: str | Document | None = Non
|
||||
@erpnext.allow_regional
|
||||
def add_regional_gl_entries(gl_entries, doc):
|
||||
return
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_bank_transactions(payment_entry: str) -> list:
|
||||
frappe.has_permission("Payment Entry", ptype="read", doc=payment_entry, throw=True)
|
||||
return frappe.get_all(
|
||||
"Bank Transaction Payments",
|
||||
filters={
|
||||
"payment_document": "Payment Entry",
|
||||
"payment_entry": payment_entry,
|
||||
},
|
||||
pluck="parent",
|
||||
)
|
||||
|
||||
@@ -1,20 +1,4 @@
|
||||
frappe.listview_settings["Payment Entry"] = {
|
||||
add_fields: ["unallocated_amount", "docstatus"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.docstatus === 2) {
|
||||
return [__("Cancelled"), "red", "docstatus,=,2"];
|
||||
}
|
||||
|
||||
if (doc.docstatus === 0) {
|
||||
return [__("Draft"), "orange", "docstatus,=,0"];
|
||||
}
|
||||
|
||||
if (flt(doc.unallocated_amount) > 0) {
|
||||
return [__("Unreconciled"), "orange", "docstatus,=,1|unallocated_amount,>,0"];
|
||||
}
|
||||
|
||||
return [__("Reconciled"), "green", "docstatus,=,1|unallocated_amount,=,0"];
|
||||
},
|
||||
onload: function (listview) {
|
||||
if (listview.page.fields_dict.party_type) {
|
||||
listview.page.fields_dict.party_type.get_query = function () {
|
||||
|
||||
@@ -1119,7 +1119,7 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
pe.save()
|
||||
|
||||
self.assertTrue("is on hold" in str(err.exception).lower())
|
||||
self.assertIn("is on hold", str(err.exception).lower())
|
||||
|
||||
def test_payment_entry_for_employee(self):
|
||||
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
|
||||
@@ -2035,8 +2035,8 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
|
||||
# check cancellation of payment entry and journal entry
|
||||
pe.cancel()
|
||||
self.assertTrue(pe.docstatus == 2)
|
||||
self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2)
|
||||
self.assertEqual(pe.docstatus, 2)
|
||||
self.assertEqual(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus"), 2)
|
||||
|
||||
# check deletion of payment entry and journal entry
|
||||
pe.delete()
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import nowdate
|
||||
from frappe.query_builder.functions import Count, Sum
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
@@ -90,6 +91,7 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
posting_date = nowdate()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
posting_date=posting_date,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
@@ -531,3 +533,82 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
|
||||
# with references removed, deletion should be possible
|
||||
so.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Accounts Settings",
|
||||
{"enable_immutable_ledger": 1},
|
||||
)
|
||||
def test_reverse_entries_on_cancel_for_immutable_ledger(self):
|
||||
invoice_posting_date = add_days(nowdate(), -5)
|
||||
gle = qb.DocType("GL Entry")
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=100, posting_date=invoice_posting_date)
|
||||
|
||||
gles_before = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
Count(gle.name),
|
||||
)
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.run()[0][0]
|
||||
)
|
||||
ples_before = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Count(ple.name),
|
||||
)
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
|
||||
.run()[0][0]
|
||||
)
|
||||
|
||||
si.cancel()
|
||||
|
||||
gles_after = (
|
||||
qb.from_(gle)
|
||||
.select(Count(gle.account))
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.run()[0][0]
|
||||
)
|
||||
self.assertEqual(gles_after, gles_before * 2)
|
||||
|
||||
ples_after = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Count(ple.name),
|
||||
)
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
|
||||
.run()[0][0]
|
||||
)
|
||||
self.assertEqual(ples_after, ples_before * 2)
|
||||
|
||||
# assert debit/credit are reversed
|
||||
gl_entries = (
|
||||
qb.from_(gle)
|
||||
.select(gle.account, Sum(gle.debit).as_("total_debit"), Sum(gle.credit).as_("total_credit"))
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.groupby(gle.account)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for gl in gl_entries:
|
||||
with self.subTest(gl=gl):
|
||||
self.assertEqual(gl.total_debit, gl.total_credit)
|
||||
|
||||
# assert amounts are reversed
|
||||
pl_entries = (
|
||||
qb.from_(ple)
|
||||
.select(ple.account, Sum(ple.amount).as_("total_amount"))
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked == 0))
|
||||
.groupby(ple.account)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for pl in pl_entries:
|
||||
with self.subTest(pl=pl):
|
||||
self.assertEqual(pl.total_amount, 0)
|
||||
|
||||
self.assertFalse(
|
||||
frappe.db.exists(
|
||||
"Payment Ledger Entry",
|
||||
{"voucher_type": si.doctype, "voucher_no": si.name, "delinked": 1},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
|
||||
from frappe.utils.data import getdate as convert_to_date
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -15,7 +13,6 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
|
||||
@@ -1293,11 +1293,16 @@ def get_open_payment_requests_query(
|
||||
)
|
||||
|
||||
return [
|
||||
(
|
||||
pr.name,
|
||||
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
|
||||
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
|
||||
)
|
||||
{
|
||||
"value": pr.name,
|
||||
"description": ", ".join(
|
||||
[
|
||||
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
|
||||
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
|
||||
]
|
||||
),
|
||||
"description_html": True,
|
||||
}
|
||||
for pr in open_payment_requests
|
||||
]
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
from erpnext.controllers.accounts_controller import get_payment_term_details
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -55,6 +57,52 @@ class TestPaymentTermsTemplate(ERPNextTestSuite):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, template.insert)
|
||||
|
||||
def test_no_discount_date_without_discount(self):
|
||||
posting_date = "2026-05-29"
|
||||
term = frappe._dict(
|
||||
{
|
||||
"payment_term": "_Test No Discount Term",
|
||||
"invoice_portion": 100.0,
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 0,
|
||||
"credit_months": 0,
|
||||
"discount_type": "Percentage",
|
||||
"discount": 0,
|
||||
"discount_validity_based_on": "Day(s) after invoice date",
|
||||
"discount_validity": 0,
|
||||
}
|
||||
)
|
||||
|
||||
details = get_payment_term_details(
|
||||
term, posting_date=posting_date, grand_total=100, base_grand_total=100
|
||||
)
|
||||
|
||||
self.assertEqual(getdate(details.due_date), getdate(posting_date))
|
||||
self.assertIsNone(details.discount_date)
|
||||
|
||||
def test_discount_date_generated_with_discount(self):
|
||||
posting_date = "2026-05-29"
|
||||
term = frappe._dict(
|
||||
{
|
||||
"payment_term": "_Test Discount Term",
|
||||
"invoice_portion": 100.0,
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"credit_days": 30,
|
||||
"credit_months": 0,
|
||||
"discount_type": "Percentage",
|
||||
"discount": 5,
|
||||
"discount_validity_based_on": "Day(s) after invoice date",
|
||||
"discount_validity": 10,
|
||||
}
|
||||
)
|
||||
|
||||
details = get_payment_term_details(
|
||||
term, posting_date=posting_date, grand_total=100, base_grand_total=100
|
||||
)
|
||||
|
||||
self.assertEqual(getdate(details.due_date), getdate(add_days(posting_date, 30)))
|
||||
self.assertEqual(getdate(details.discount_date), getdate(add_days(posting_date, 10)))
|
||||
|
||||
def test_duplicate_terms(self):
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.utils import today
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
@@ -333,6 +334,48 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
|
||||
return pcv
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Accounts Settings",
|
||||
{"enable_immutable_ledger": 1},
|
||||
)
|
||||
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
jv = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=400,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv.company = company
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
|
||||
# Passed posting_date is after PCV end date, so cancellation should not fail.
|
||||
make_reverse_gl_entries(
|
||||
voucher_type="Journal Entry",
|
||||
voucher_no=jv.name,
|
||||
posting_date="2022-01-01",
|
||||
)
|
||||
|
||||
totals_after_cancel = frappe.db.sql(
|
||||
"""
|
||||
select sum(debit) as total_debit, sum(credit) as total_credit
|
||||
from `tabGL Entry`
|
||||
where voucher_type=%s and voucher_no=%s and is_cancelled=0
|
||||
""",
|
||||
("Journal Entry", jv.name),
|
||||
as_dict=True,
|
||||
)[0]
|
||||
|
||||
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
|
||||
|
||||
|
||||
def create_company():
|
||||
company = frappe.get_doc(
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
"due_date",
|
||||
"amended_from",
|
||||
"return_against",
|
||||
"section_break_clmv",
|
||||
"title",
|
||||
"accounting_dimensions_section",
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
@@ -172,6 +170,7 @@
|
||||
"is_discounted",
|
||||
"col_break23",
|
||||
"status",
|
||||
"title",
|
||||
"more_info",
|
||||
"debit_to",
|
||||
"party_account_currency",
|
||||
@@ -1625,10 +1624,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_clmv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
@@ -1641,7 +1636,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-01 02:37:30.580568",
|
||||
"modified": "2026-05-28 12:22:50.253090",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -59,7 +59,7 @@ class TestPOSInvoiceMergeLog(ERPNextTestSuite):
|
||||
pos_inv3.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
|
||||
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv3.consolidated_invoice)
|
||||
|
||||
def test_consolidated_credit_note_creation(self):
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
@@ -454,12 +454,12 @@ class TestPOSInvoiceMergeLog(ERPNextTestSuite):
|
||||
pos_inv2.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice))
|
||||
|
||||
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv3.consolidated_invoice)
|
||||
|
||||
pos_inv3.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
|
||||
self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
self.assertEqual(pos_inv2.consolidated_invoice, pos_inv3.consolidated_invoice)
|
||||
|
||||
def test_company_in_pos_invoice_merge_log(self):
|
||||
"""
|
||||
|
||||
@@ -209,15 +209,14 @@ class POSProfile(Document):
|
||||
def set_defaults(self, include_current_pos=True):
|
||||
frappe.defaults.clear_default("is_pos")
|
||||
|
||||
if not include_current_pos:
|
||||
condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'")
|
||||
else:
|
||||
condition = " where pfu.default = 1 "
|
||||
pfu = frappe.qb.DocType("POS Profile User")
|
||||
|
||||
pos_view_users = frappe.db.sql_list(
|
||||
f"""select pfu.user
|
||||
from `tabPOS Profile User` as pfu {condition}"""
|
||||
)
|
||||
query = frappe.qb.from_(pfu).select(pfu.user).where(pfu.default == 1)
|
||||
|
||||
if not include_current_pos:
|
||||
query = query.where(pfu.name != self.name)
|
||||
|
||||
pos_view_users = query.run(as_list=1, pluck=True)
|
||||
|
||||
for user in pos_view_users:
|
||||
if user:
|
||||
|
||||
@@ -151,13 +151,13 @@
|
||||
"label": "Default Advance Account",
|
||||
"mandatory_depends_on": "doc.party_type",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-08 08:22:14.798085",
|
||||
"modified": "2026-05-16 11:43:12.758685",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
|
||||
@@ -23,7 +23,7 @@ class ProcessPaymentReconciliation(Document):
|
||||
bank_cash_account: DF.Link | None
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
default_advance_account: DF.Link
|
||||
default_advance_account: DF.Link | None
|
||||
error_log: DF.LongText | None
|
||||
from_invoice_date: DF.Date | None
|
||||
from_payment_date: DF.Date | None
|
||||
@@ -218,10 +218,7 @@ def trigger_reconciliation_for_queued_docs():
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
|
||||
|
||||
def get_filters_as_tuple(fields, doc):
|
||||
filters = ()
|
||||
for x in fields:
|
||||
filters += tuple(doc.get(x))
|
||||
return filters
|
||||
return tuple(doc.get(x) or "" for x in fields)
|
||||
|
||||
for x in all_queued:
|
||||
doc = frappe.get_doc("Process Payment Reconciliation", x)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "format:Process-PCV-{###}",
|
||||
"creation": "2025-09-25 15:44:03.534699",
|
||||
"doctype": "DocType",
|
||||
@@ -7,11 +8,13 @@
|
||||
"field_order": [
|
||||
"parent_pcv",
|
||||
"status",
|
||||
"amended_from",
|
||||
"section_normal_balances",
|
||||
"p_l_closing_balance",
|
||||
"normal_balances",
|
||||
"bs_closing_balance",
|
||||
"z_opening_balances",
|
||||
"amended_from"
|
||||
"normal_balances",
|
||||
"section_opening_balances",
|
||||
"z_opening_balances"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -64,17 +67,27 @@
|
||||
"fieldname": "bs_closing_balance",
|
||||
"fieldtype": "JSON",
|
||||
"label": "Balance Sheet Closing Balance"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_normal_balances",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Normal Balances"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_opening_balances",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Opening Balances"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-05 11:40:24.996403",
|
||||
"modified": "2026-06-01 12:16:37.374412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Period Closing Voucher",
|
||||
"naming_rule": "Expression",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -36,8 +36,8 @@ 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")
|
||||
|
||||
@@ -72,8 +72,8 @@ class ProcessPeriodClosingVoucher(Document):
|
||||
pcv = frappe.get_doc("Period Closing Voucher", self.parent_pcv)
|
||||
if pcv.is_first_period_closing_voucher():
|
||||
gl = qb.DocType("GL Entry")
|
||||
min = qb.from_(gl).select(Min(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
|
||||
max = qb.from_(gl).select(Max(gl.posting_date)).where(gl.company.eq(pcv.company)).run()[0][0]
|
||||
min = qb.from_(gl).select(Min(gl.posting_date)).run()[0][0]
|
||||
max = qb.from_(gl).select(Max(gl.posting_date)).run()[0][0]
|
||||
|
||||
dates = self.get_dates(get_datetime(min), get_datetime(max))
|
||||
for x in dates:
|
||||
@@ -93,12 +93,16 @@ class ProcessPeriodClosingVoucher(Document):
|
||||
def start_pcv_processing(docname: str):
|
||||
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
|
||||
if normal_balances := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
fields=["processing_date", "report_type", "parentfield"],
|
||||
order_by="parentfield, idx, processing_date",
|
||||
limit=4,
|
||||
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if normal_balances := (
|
||||
qb.from_(ppcvd)
|
||||
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
|
||||
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
|
||||
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
|
||||
.limit(4)
|
||||
.for_update(skip_locked=True)
|
||||
.run(as_dict=True)
|
||||
):
|
||||
if not is_scheduler_inactive():
|
||||
for x in normal_balances:
|
||||
@@ -133,9 +137,10 @@ def pause_pcv_processing(docname: str):
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
# If a date is stuck in 'Running' state, this will allow it to procced.
|
||||
if queued_dates := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
filters={"parent": docname, "status": ["in", ["Queued", "Running"]]},
|
||||
pluck="name",
|
||||
):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
@@ -169,6 +174,9 @@ def resume_pcv_processing(docname: str):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
|
||||
start_pcv_processing(docname)
|
||||
else:
|
||||
# If a parent doc is stuck in 'Running' state, will allow it to proceed.
|
||||
schedule_next_date(docname)
|
||||
|
||||
|
||||
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
|
||||
@@ -238,12 +246,15 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
|
||||
|
||||
@frappe.whitelist()
|
||||
def schedule_next_date(docname: str):
|
||||
if to_process := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
fields=["processing_date", "report_type", "parentfield"],
|
||||
order_by="parentfield, idx, processing_date",
|
||||
limit=1,
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if to_process := (
|
||||
qb.from_(ppcvd)
|
||||
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
|
||||
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
|
||||
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
|
||||
.limit(1)
|
||||
.for_update(skip_locked=True)
|
||||
.run(as_dict=True)
|
||||
):
|
||||
if not is_scheduler_inactive():
|
||||
frappe.db.set_value(
|
||||
@@ -281,7 +292,21 @@ def schedule_next_date(docname: str):
|
||||
)
|
||||
# Ensure both normal and opening balances are processed for all dates
|
||||
if total_no_of_dates == completed:
|
||||
summarize_and_post_ledger_entries(docname)
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_job_running,
|
||||
)
|
||||
|
||||
job_name = f"summarize_{docname}"
|
||||
if not is_job_running(job_name):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
)
|
||||
|
||||
|
||||
def make_dict_json_compliant(dimension_wise_balance) -> dict:
|
||||
@@ -537,6 +562,9 @@ def process_individual_date(docname: str, date, report_type, parentfield):
|
||||
|
||||
if parentfield == "z_opening_balances":
|
||||
query = query.where(gle.is_opening.eq("Yes"))
|
||||
else:
|
||||
# Keep balances aligned with legacy PCV logic (non-opening transactions only)
|
||||
query = query.where(gle.is_opening.eq("No"))
|
||||
|
||||
query = query.groupby(gle.account)
|
||||
for dim in dimensions:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// render
|
||||
frappe.listview_settings["Process Period Closing Voucher"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
const status_colors = {
|
||||
Queued: "blue",
|
||||
Running: "orange",
|
||||
Paused: "gray",
|
||||
Completed: "green",
|
||||
Cancelled: "red",
|
||||
};
|
||||
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,173 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher import (
|
||||
process_individual_date,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 0)
|
||||
self.company = "_Test Company"
|
||||
|
||||
def make_period_closing_voucher(self, posting_date, submit=True):
|
||||
fy = get_fiscal_year(posting_date, company="_Test Company")
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": posting_date or today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": fy[2],
|
||||
"company": self.company,
|
||||
"fiscal_year": fy[0],
|
||||
"closing_account_head": "Retained Earnings - _TC",
|
||||
"remarks": "closing",
|
||||
}
|
||||
)
|
||||
pcv.insert()
|
||||
if submit:
|
||||
pcv.submit()
|
||||
|
||||
return pcv
|
||||
|
||||
def make_process_pcv(self):
|
||||
self.pcv = self.make_period_closing_voucher(posting_date=today(), submit=False)
|
||||
ppcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Process Period Closing Voucher",
|
||||
"parent_pcv": self.pcv.name,
|
||||
}
|
||||
)
|
||||
ppcv.save()
|
||||
return ppcv
|
||||
|
||||
def set_processing_date_status(self, date, ppcv, rpt_type, parentfield, status):
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
|
||||
"status",
|
||||
status,
|
||||
)
|
||||
|
||||
def get_processing_date_closing_balance(self, date, ppcv, rpt_type, parentfield):
|
||||
return frappe.db.get_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
|
||||
"closing_balance",
|
||||
)
|
||||
|
||||
def test_opening_balance_double_counting(self):
|
||||
ppcv = self.make_process_pcv()
|
||||
self.assertEqual(self.pcv.is_first_period_closing_voucher(), True)
|
||||
opening_jv = make_journal_entry(
|
||||
posting_date=today(),
|
||||
amount=10,
|
||||
account1="Cash - _TC",
|
||||
account2="Debtors - _TC",
|
||||
company=self.company,
|
||||
save=False,
|
||||
)
|
||||
opening_jv.accounts[1].party_type = "Customer"
|
||||
opening_jv.accounts[1].party = "_Test Customer"
|
||||
opening_jv.is_opening = "Yes"
|
||||
opening_jv.save()
|
||||
opening_jv.submit()
|
||||
|
||||
jv = make_journal_entry(
|
||||
posting_date=today(),
|
||||
amount=120,
|
||||
account1="Debtors - _TC",
|
||||
account2="Sales - _TC",
|
||||
company=self.company,
|
||||
save=False,
|
||||
)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = "_Test Customer"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
# P&L balance
|
||||
parentfield = "normal_balances"
|
||||
rpt_type = "Profit and Loss"
|
||||
# status has to be set to 'Running' for logic to run
|
||||
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
|
||||
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(
|
||||
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
|
||||
)
|
||||
self.assertEqual(len(bal), 1)
|
||||
expected_pl = {
|
||||
"account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"debit": 0.0,
|
||||
"credit": 120.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"credit_in_account_currency": 120.0,
|
||||
}
|
||||
for k in expected_pl.keys():
|
||||
with self.subTest(k):
|
||||
self.assertEqual(expected_pl[k], bal[0][k])
|
||||
|
||||
# Balance sheet balance
|
||||
rpt_type = "Balance Sheet"
|
||||
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
|
||||
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(
|
||||
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
|
||||
)
|
||||
self.assertEqual(len(bal), 1)
|
||||
expected_bs = {
|
||||
"account": "Debtors - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"debit": 120.0,
|
||||
"credit": 0.0,
|
||||
"debit_in_account_currency": 120.0,
|
||||
"credit_in_account_currency": 0.0,
|
||||
}
|
||||
for k in expected_bs.keys():
|
||||
with self.subTest(k):
|
||||
self.assertEqual(expected_bs[k], bal[0][k])
|
||||
|
||||
# Opening balance
|
||||
parentfield = "z_opening_balances"
|
||||
rpt_type = "Balance Sheet"
|
||||
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
|
||||
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(
|
||||
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
|
||||
)
|
||||
self.assertEqual(len(bal), 2)
|
||||
opening_cash = next(x for x in bal if x["account"] == "Cash - _TC")
|
||||
expected_opening_cash = {
|
||||
"account": "Cash - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"debit": 10.0,
|
||||
"credit": 0.0,
|
||||
"debit_in_account_currency": 10.0,
|
||||
"credit_in_account_currency": 0.0,
|
||||
"account_currency": "INR",
|
||||
}
|
||||
for k in expected_opening_cash.keys():
|
||||
with self.subTest(k):
|
||||
self.assertEqual(expected_opening_cash[k], opening_cash[k])
|
||||
|
||||
opening_debtors = next(x for x in bal if x["account"] == "Debtors - _TC")
|
||||
expected_opening_debtors = {
|
||||
"account": "Debtors - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"debit": 0.0,
|
||||
"credit": 10.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"credit_in_account_currency": 10.0,
|
||||
"account_currency": "INR",
|
||||
}
|
||||
for k in expected_opening_debtors.keys():
|
||||
with self.subTest(k):
|
||||
self.assertEqual(expected_opening_debtors[k], opening_debtors[k])
|
||||
|
||||
@@ -591,6 +591,25 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["items"].grid.get_field("deferred_expense_account").get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
"update_billed_amount_in_purchase_receipt",
|
||||
"apply_tds",
|
||||
"amended_from",
|
||||
"section_break_ecfi",
|
||||
"title",
|
||||
"supplier_invoice_details",
|
||||
"bill_no",
|
||||
"column_break_15",
|
||||
@@ -202,6 +200,7 @@
|
||||
"hold_comment",
|
||||
"additional_info_section",
|
||||
"is_internal_supplier",
|
||||
"title",
|
||||
"represents_company",
|
||||
"supplier_group",
|
||||
"sender",
|
||||
@@ -1684,10 +1683,6 @@
|
||||
"fieldname": "automation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Automation"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ecfi",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1695,7 +1690,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-04 10:10:11.717131",
|
||||
"modified": "2026-05-28 12:36:55.215363",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -36,7 +36,6 @@ from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
@@ -282,7 +281,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.check_conversion_rate()
|
||||
self.validate_credit_to_acc()
|
||||
self.clear_unallocated_advances("Purchase Invoice Advance", "advances")
|
||||
self.check_on_hold_or_closed_status()
|
||||
self.check_for_on_hold_or_closed_status(
|
||||
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
|
||||
)
|
||||
self.validate_with_previous_doc()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
@@ -290,6 +291,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.validate_expense_account()
|
||||
self.set_against_expense_account()
|
||||
self.validate_write_off_account()
|
||||
self.validate_write_off_cost_center()
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
@@ -387,14 +389,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.party_account_currency = account.account_currency
|
||||
|
||||
def check_on_hold_or_closed_status(self):
|
||||
check_list = []
|
||||
|
||||
for d in self.get("items"):
|
||||
if d.purchase_order and d.purchase_order not in check_list and not d.purchase_receipt:
|
||||
check_list.append(d.purchase_order)
|
||||
check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super().validate_with_previous_doc(
|
||||
{
|
||||
@@ -635,15 +629,16 @@ class PurchaseInvoice(BuyingController):
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
|
||||
def pr_required(self):
|
||||
stock_items = self.get_stock_items()
|
||||
if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes":
|
||||
stock_and_asset_items = self.get_stock_items()
|
||||
stock_and_asset_items.extend(self.get_asset_items())
|
||||
if frappe.get_value(
|
||||
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
|
||||
):
|
||||
return
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.purchase_receipt and d.item_code in stock_items:
|
||||
if not d.purchase_receipt and d.item_code in stock_and_asset_items:
|
||||
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
|
||||
msg += "<br><br>"
|
||||
msg += _(
|
||||
@@ -659,6 +654,27 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.write_off_amount and not self.write_off_account:
|
||||
throw(_("Please enter Write Off Account"))
|
||||
|
||||
if not self.write_off_account:
|
||||
return
|
||||
|
||||
doc = frappe.db.get_value(
|
||||
"Account", self.write_off_account, ["report_type", "is_group", "company"], as_dict=True
|
||||
)
|
||||
|
||||
if not doc or doc.report_type != "Profit and Loss" or doc.is_group or doc.company != self.company:
|
||||
throw(_("Please enter a valid Write Off Account"))
|
||||
|
||||
def validate_write_off_cost_center(self):
|
||||
if not self.write_off_cost_center:
|
||||
return
|
||||
|
||||
doc = frappe.db.get_value(
|
||||
"Cost Center", self.write_off_cost_center, ["is_group", "company"], as_dict=True
|
||||
)
|
||||
|
||||
if not doc or doc.is_group or doc.company != self.company:
|
||||
throw(_("Please enter a valid Write Off Cost Center"))
|
||||
|
||||
def check_prev_docstatus(self):
|
||||
for d in self.get("items"):
|
||||
if d.purchase_order:
|
||||
@@ -739,6 +755,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_write_off_cost_center()
|
||||
self.validate_expense_account()
|
||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
@@ -849,7 +866,9 @@ class PurchaseInvoice(BuyingController):
|
||||
if update_outstanding == "No":
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
voucher_no=self.return_against
|
||||
if (cint(self.is_return) and self.return_against)
|
||||
else self.name,
|
||||
account=self.credit_to,
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
@@ -1543,6 +1562,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def make_payment_gl_entries(self, gl_entries):
|
||||
# Make Cash GL Entries
|
||||
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
bank_account_currency = get_account_currency(self.cash_bank_account)
|
||||
# CASH, make payment entries
|
||||
gl_entries.append(
|
||||
@@ -1557,9 +1579,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"debit_in_transaction_currency": self.paid_amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
@@ -1681,7 +1701,9 @@ class PurchaseInvoice(BuyingController):
|
||||
super().on_cancel()
|
||||
PurchaseTaxWithholding(self).on_cancel()
|
||||
|
||||
self.check_on_hold_or_closed_status()
|
||||
self.check_for_on_hold_or_closed_status(
|
||||
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
|
||||
)
|
||||
|
||||
if self.is_return and not self.update_billed_amount_in_purchase_order:
|
||||
# NOTE status updating bypassed for is_return
|
||||
|
||||
@@ -2077,7 +2077,7 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||
return_pi.save().submit()
|
||||
|
||||
self.assertTrue(return_pi.docstatus == 1)
|
||||
self.assertEqual(return_pi.docstatus, 1)
|
||||
|
||||
def test_advance_entries_as_asset(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
@@ -2962,6 +2962,52 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
pr = make_purchase_receipt_from_pi(pi.name)
|
||||
self.assertFalse(pr.items)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_purchase_invoice_return_common_party_je_has_no_negative_amounts(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
customer = make_customer(customer="_Test Common Party Return PI")
|
||||
supplier = create_supplier(supplier_name="_Test Common Party Return PI").name
|
||||
# Supplier must be secondary so get_common_party_link finds it via the PI's party_type
|
||||
party_link = create_party_link("Customer", customer, supplier)
|
||||
|
||||
pi = make_purchase_invoice(supplier=supplier, parent_cost_center="_Test Cost Center - _TC")
|
||||
|
||||
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||
return_pi.submit()
|
||||
|
||||
# JE for the return should credit the supplier (secondary/reconciliation) account
|
||||
# and debit the customer (primary) account — all positive amounts
|
||||
jv_accounts = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": return_pi.doctype, "reference_name": return_pi.name, "docstatus": 1},
|
||||
fields=["debit_in_account_currency", "credit_in_account_currency", "account"],
|
||||
)
|
||||
|
||||
self.assertTrue(jv_accounts, "Expected a Journal Entry for the return invoice")
|
||||
for row in jv_accounts:
|
||||
self.assertGreaterEqual(
|
||||
row.debit_in_account_currency,
|
||||
0,
|
||||
f"Negative debit on account {row.account}",
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
row.credit_in_account_currency,
|
||||
0,
|
||||
f"Negative credit on account {row.account}",
|
||||
)
|
||||
|
||||
# Supplier (secondary) account must be credited, not debited
|
||||
supplier_row = next(r for r in jv_accounts if r.account == pi.credit_to)
|
||||
self.assertGreater(supplier_row.credit_in_account_currency, 0)
|
||||
self.assertEqual(supplier_row.debit_in_account_currency, 0)
|
||||
|
||||
party_link.delete()
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
"is_created_using_pos",
|
||||
"pos_closing_entry",
|
||||
"has_subcontracted",
|
||||
"section_break_sgnf",
|
||||
"title",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -232,6 +230,7 @@
|
||||
"status",
|
||||
"remarks",
|
||||
"customer_group",
|
||||
"title",
|
||||
"column_break_imbx",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
@@ -1919,7 +1918,7 @@
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: !doc.is_return",
|
||||
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
|
||||
"description": "Issue a debit note against an existing Sales Invoice to adjust the rate. The quantity will be retained from the original invoice.",
|
||||
"fieldname": "is_debit_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Rate Adjustment Entry (Debit Note)"
|
||||
@@ -2333,10 +2332,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Automation"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_sgnf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
@@ -2357,7 +2352,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2026-05-01 02:37:29.742764",
|
||||
"modified": "2026-05-28 12:15:12.486443",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -29,7 +29,12 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import SalesTaxWithholding
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.party import (
|
||||
CROSS_PARTY_FIELD_NO_MAP,
|
||||
_get_party_details,
|
||||
get_due_date,
|
||||
get_party_account,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
@@ -969,9 +974,6 @@ class SalesInvoice(SellingController):
|
||||
if selling_price_list:
|
||||
self.set("selling_price_list", selling_price_list)
|
||||
|
||||
if not for_validate:
|
||||
self.update_stock = cint(pos.get("update_stock"))
|
||||
|
||||
# set pos values in items
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
@@ -982,6 +984,10 @@ class SalesInvoice(SellingController):
|
||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||
item.set(fname, val)
|
||||
|
||||
if not for_validate:
|
||||
dn_flag = any(d.get("dn_detail") for d in self.get("items"))
|
||||
self.update_stock = 0 if dn_flag else cint(pos.get("update_stock"))
|
||||
|
||||
# fetch terms
|
||||
if self.tc_name and not self.terms:
|
||||
self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms")
|
||||
@@ -993,7 +999,7 @@ class SalesInvoice(SellingController):
|
||||
return pos
|
||||
|
||||
def get_company_abbr(self):
|
||||
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
|
||||
return frappe.db.get_value("Company", self.company, "abbr")
|
||||
|
||||
def validate_debit_to_acc(self):
|
||||
if not self.debit_to:
|
||||
@@ -1033,11 +1039,7 @@ class SalesInvoice(SellingController):
|
||||
def clear_unallocated_mode_of_payments(self):
|
||||
self.set("payments", self.get("payments", {"amount": ["not in", [0, None, ""]]}))
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSales Invoice Payment` where parent = %s
|
||||
and amount = 0""",
|
||||
self.name,
|
||||
)
|
||||
frappe.db.delete("Sales Invoice Payment", filters={"parent": self.name, "amount": 0})
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super().validate_with_previous_doc(
|
||||
@@ -1133,12 +1135,20 @@ class SalesInvoice(SellingController):
|
||||
def validate_proj_cust(self):
|
||||
"""check for does customer belong to same project as entered.."""
|
||||
if self.project and self.customer:
|
||||
res = frappe.db.sql(
|
||||
"""select name from `tabProject`
|
||||
where name = %s and (customer = %s or customer is null or customer = '')""",
|
||||
(self.project, self.customer),
|
||||
Project = frappe.qb.DocType("Project")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Project)
|
||||
.select(Project.name)
|
||||
.where(Project.name == self.project)
|
||||
.where(
|
||||
(Project.customer == self.customer)
|
||||
| (Project.customer.isnull())
|
||||
| (Project.customer == "")
|
||||
)
|
||||
)
|
||||
if not res:
|
||||
|
||||
if not query.run():
|
||||
throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project))
|
||||
|
||||
def validate_pos(self):
|
||||
@@ -1363,19 +1373,28 @@ class SalesInvoice(SellingController):
|
||||
self.total_billing_hours = timesheet_sum("billing_hours")
|
||||
|
||||
def get_warehouse(self):
|
||||
user_pos_profile = frappe.db.sql(
|
||||
"""select name, warehouse from `tabPOS Profile`
|
||||
where ifnull(user,'') = %s and company = %s""",
|
||||
(frappe.session["user"], self.company),
|
||||
POSProfile = frappe.qb.DocType("POS Profile")
|
||||
|
||||
user_query = (
|
||||
frappe.qb.from_(POSProfile)
|
||||
.select(POSProfile.name, POSProfile.warehouse)
|
||||
.where(POSProfile.company == self.company)
|
||||
.where(
|
||||
(POSProfile.user == frappe.session["user"])
|
||||
| ((POSProfile.user.isnull() | (POSProfile.user == "")) & (frappe.session["user"] == ""))
|
||||
)
|
||||
)
|
||||
user_pos_profile = user_query.run()
|
||||
warehouse = user_pos_profile[0][1] if user_pos_profile else None
|
||||
|
||||
if not warehouse:
|
||||
global_pos_profile = frappe.db.sql(
|
||||
"""select name, warehouse from `tabPOS Profile`
|
||||
where (user is null or user = '') and company = %s""",
|
||||
self.company,
|
||||
global_query = (
|
||||
frappe.qb.from_(POSProfile)
|
||||
.select(POSProfile.name, POSProfile.warehouse)
|
||||
.where(POSProfile.company == self.company)
|
||||
.where(POSProfile.user.isnull() | (POSProfile.user == ""))
|
||||
)
|
||||
global_pos_profile = global_query.run()
|
||||
|
||||
if global_pos_profile:
|
||||
warehouse = global_pos_profile[0][1]
|
||||
@@ -1586,6 +1605,12 @@ class SalesInvoice(SellingController):
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
|
||||
disable_sdbnb_in_sr = frappe.get_cached_value("Company", self.company, "disable_sdbnb_in_sr")
|
||||
|
||||
if not (self.is_return and disable_sdbnb_in_sr):
|
||||
self.stock_delivered_but_not_billed_gl_entries(gl_entries)
|
||||
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
@@ -1603,6 +1628,81 @@ class SalesInvoice(SellingController):
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def stock_delivered_but_not_billed_gl_entries(self, gl_entries):
|
||||
if self.update_stock or not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
||||
return
|
||||
|
||||
for item in self.get("items"):
|
||||
if not item.delivery_note and not item.dn_detail:
|
||||
continue
|
||||
|
||||
if not frappe.get_cached_value("Item", item.item_code, "is_stock_item"):
|
||||
continue
|
||||
|
||||
dn_expense_account = frappe.get_cached_value(
|
||||
"Delivery Note Item", item.dn_detail, "expense_account"
|
||||
)
|
||||
if (
|
||||
not dn_expense_account
|
||||
or frappe.get_cached_value("Account", dn_expense_account, "account_type")
|
||||
!= "Stock Delivered But Not Billed"
|
||||
or not item.expense_account
|
||||
or dn_expense_account == item.expense_account
|
||||
):
|
||||
continue
|
||||
|
||||
delivery_note = item.delivery_note or frappe.get_cached_value(
|
||||
"Delivery Note Item", item.dn_detail, "parent"
|
||||
)
|
||||
if not delivery_note:
|
||||
continue
|
||||
|
||||
item_g = frappe.get_cached_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_no": delivery_note,
|
||||
"voucher_detail_no": item.dn_detail,
|
||||
"item_code": item.item_code,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
["stock_value_difference", "actual_qty"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if not item_g or not flt(item_g.actual_qty):
|
||||
continue
|
||||
valuation_rate = flt(item_g.stock_value_difference) / flt(item_g.actual_qty)
|
||||
valuation_amount = valuation_rate * item.stock_qty
|
||||
dn_account_currency = get_account_currency(dn_expense_account)
|
||||
item_account_currency = get_account_currency(item.expense_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": dn_expense_account,
|
||||
"against": item.expense_account,
|
||||
"credit": flt(valuation_amount),
|
||||
"credit_in_account_currency": flt(valuation_amount),
|
||||
"cost_center": item.cost_center,
|
||||
},
|
||||
dn_account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item.expense_account,
|
||||
"against": dn_expense_account,
|
||||
"debit": flt(valuation_amount),
|
||||
"debit_in_account_currency": flt(valuation_amount),
|
||||
"cost_center": item.cost_center,
|
||||
},
|
||||
item_account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
def make_customer_gl_entry(self, gl_entries):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introduction of posting GLE based on rounded total
|
||||
@@ -2024,15 +2124,24 @@ class SalesInvoice(SellingController):
|
||||
def update_billing_status_in_dn(self, update_modified=True):
|
||||
if self.is_return and not self.update_billed_amount_in_delivery_note:
|
||||
return
|
||||
|
||||
updated_delivery_notes = []
|
||||
|
||||
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
|
||||
for d in self.get("items"):
|
||||
if d.dn_detail:
|
||||
billed_amt = frappe.db.sql(
|
||||
"""select sum(amount) from `tabSales Invoice Item`
|
||||
where dn_detail=%s and docstatus=1""",
|
||||
d.dn_detail,
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoiceItem)
|
||||
.select(Coalesce(Sum(SalesInvoiceItem.amount), 0))
|
||||
.where(SalesInvoiceItem.dn_detail == d.dn_detail)
|
||||
.where(SalesInvoiceItem.docstatus == 1)
|
||||
)
|
||||
billed_amt = billed_amt and billed_amt[0][0] or 0
|
||||
|
||||
res = query.run()
|
||||
billed_amt = res[0][0] if res else 0
|
||||
|
||||
frappe.db.set_value(
|
||||
"Delivery Note Item",
|
||||
d.dn_detail,
|
||||
@@ -2114,28 +2223,29 @@ class SalesInvoice(SellingController):
|
||||
|
||||
# valdite the redemption and then delete the loyalty points earned on cancel of the invoice
|
||||
def delete_loyalty_point_entry(self):
|
||||
lp_entry = frappe.db.sql(
|
||||
"select name from `tabLoyalty Point Entry` where invoice=%s", (self.name), as_dict=1
|
||||
lp_entry = frappe.db.get_all(
|
||||
"Loyalty Point Entry", filters={"invoice": self.name, "loyalty_points": (">", 0)}, fields=["name"]
|
||||
)
|
||||
|
||||
if not lp_entry:
|
||||
return
|
||||
against_lp_entry = frappe.db.sql(
|
||||
"""select name, invoice from `tabLoyalty Point Entry`
|
||||
where redeem_against=%s""",
|
||||
(lp_entry[0].name),
|
||||
as_dict=1,
|
||||
|
||||
against_lp_entry = frappe.db.get_all(
|
||||
"Loyalty Point Entry",
|
||||
filters={"redeem_against": lp_entry[0].name},
|
||||
fields=["name", "invoice"],
|
||||
)
|
||||
|
||||
if against_lp_entry:
|
||||
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}"""
|
||||
"{} can't be cancelled since the Loyalty Points earned has been redeemed. "
|
||||
"First cancel the {} No {}"
|
||||
).format(self.doctype, self.doctype, invoice_list)
|
||||
)
|
||||
else:
|
||||
frappe.db.sql("""delete from `tabLoyalty Point Entry` where invoice=%s""", (self.name))
|
||||
# Set loyalty program
|
||||
frappe.db.delete("Loyalty Point Entry", filters={"invoice": self.name})
|
||||
self.set_loyalty_program_tier()
|
||||
|
||||
def set_loyalty_program_tier(self):
|
||||
@@ -2306,19 +2416,21 @@ def is_overdue(doc, total):
|
||||
def get_discounting_status(sales_invoice):
|
||||
status = None
|
||||
|
||||
invoice_discounting_list = frappe.db.sql(
|
||||
"""
|
||||
select status
|
||||
from `tabInvoice Discounting` id, `tabDiscounted Invoice` d
|
||||
where
|
||||
id.name = d.parent
|
||||
and d.sales_invoice=%s
|
||||
and id.docstatus=1
|
||||
and status in ('Disbursed', 'Settled')
|
||||
""",
|
||||
sales_invoice,
|
||||
InvoiceDiscounting = frappe.qb.DocType("Invoice Discounting")
|
||||
DiscountedInvoice = frappe.qb.DocType("Discounted Invoice")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(InvoiceDiscounting)
|
||||
.join(DiscountedInvoice)
|
||||
.on(InvoiceDiscounting.name == DiscountedInvoice.parent)
|
||||
.select(InvoiceDiscounting.status)
|
||||
.where(DiscountedInvoice.sales_invoice == sales_invoice)
|
||||
.where(InvoiceDiscounting.docstatus == 1)
|
||||
.where(InvoiceDiscounting.status.isin(["Disbursed", "Settled"]))
|
||||
)
|
||||
|
||||
invoice_discounting_list = query.run()
|
||||
|
||||
for d in invoice_discounting_list:
|
||||
status = d[0]
|
||||
if status == "Disbursed":
|
||||
@@ -2776,7 +2888,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"set_target_warehouse": "set_from_warehouse",
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"],
|
||||
},
|
||||
doctype + " Item": item_field_map,
|
||||
},
|
||||
@@ -2936,7 +3048,7 @@ def update_taxes(
|
||||
master_doctype=None,
|
||||
):
|
||||
# Update Party Details
|
||||
party_details = get_party_details(
|
||||
party_details = _get_party_details(
|
||||
party=party,
|
||||
party_type=party_type,
|
||||
company=company,
|
||||
@@ -3036,48 +3148,65 @@ def update_multi_mode_option(doc, pos_profile):
|
||||
|
||||
|
||||
def get_all_mode_of_payments(doc):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select mpa.default_account, mpa.parent, mp.type as type
|
||||
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
||||
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
|
||||
{"company": doc.company},
|
||||
as_dict=1,
|
||||
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
|
||||
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ModeOfPaymentAccount)
|
||||
.join(ModeOfPayment)
|
||||
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
|
||||
.select(
|
||||
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
|
||||
)
|
||||
.where(ModeOfPaymentAccount.company == doc.company)
|
||||
.where(ModeOfPayment.enabled == 1)
|
||||
)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def get_mode_of_payments_info(mode_of_payments, company):
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
mpa.default_account, mpa.parent as mop, mp.type as type
|
||||
from
|
||||
`tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
||||
where
|
||||
mpa.parent = mp.name and
|
||||
mpa.company = %s and
|
||||
mp.enabled = 1 and
|
||||
mp.name in %s
|
||||
group by
|
||||
mp.name
|
||||
""",
|
||||
(company, mode_of_payments),
|
||||
as_dict=1,
|
||||
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
|
||||
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ModeOfPaymentAccount)
|
||||
.join(ModeOfPayment)
|
||||
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
|
||||
.select(
|
||||
ModeOfPaymentAccount.default_account,
|
||||
ModeOfPaymentAccount.parent.as_("mop"),
|
||||
ModeOfPayment.type.as_("type"),
|
||||
)
|
||||
.where(ModeOfPaymentAccount.company == company)
|
||||
.where(ModeOfPayment.enabled == 1)
|
||||
.where(ModeOfPayment.name.isin(mode_of_payments))
|
||||
.groupby(ModeOfPayment.name)
|
||||
)
|
||||
|
||||
data = query.run(as_dict=1)
|
||||
|
||||
return {row.get("mop"): row for row in data}
|
||||
|
||||
|
||||
def get_mode_of_payment_info(mode_of_payment, company):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select mpa.default_account, mpa.parent, mp.type as type
|
||||
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
||||
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
|
||||
(company, mode_of_payment),
|
||||
as_dict=1,
|
||||
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
|
||||
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(ModeOfPayment)
|
||||
.join(ModeOfPaymentAccount)
|
||||
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
|
||||
.select(
|
||||
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
|
||||
)
|
||||
.where(ModeOfPaymentAccount.company == company)
|
||||
.where(ModeOfPayment.enabled == 1)
|
||||
.where(ModeOfPayment.name == mode_of_payment)
|
||||
)
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dunning(
|
||||
@@ -3136,37 +3265,36 @@ def create_dunning(
|
||||
def check_if_return_invoice_linked_with_payment_entry(self):
|
||||
# If a Return invoice is linked with payment entry along with other invoices,
|
||||
# the cancellation of the Return causes allocated amount to be greater than paid
|
||||
|
||||
if not frappe.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
|
||||
return
|
||||
|
||||
payment_entries = []
|
||||
if self.is_return and self.return_against:
|
||||
invoice = self.return_against
|
||||
else:
|
||||
invoice = self.name
|
||||
|
||||
payment_entries = frappe.db.sql_list(
|
||||
"""
|
||||
SELECT
|
||||
t1.name
|
||||
FROM
|
||||
`tabPayment Entry` t1, `tabPayment Entry Reference` t2
|
||||
WHERE
|
||||
t1.name = t2.parent
|
||||
and t1.docstatus = 1
|
||||
and t2.reference_name = %s
|
||||
and t2.allocated_amount < 0
|
||||
""",
|
||||
invoice,
|
||||
PaymentEntry = frappe.qb.DocType("Payment Entry")
|
||||
PaymentEntryReference = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PaymentEntry)
|
||||
.join(PaymentEntryReference)
|
||||
.on(PaymentEntry.name == PaymentEntryReference.parent)
|
||||
.select(PaymentEntry.name)
|
||||
.where(PaymentEntry.docstatus == 1)
|
||||
.where(PaymentEntryReference.reference_name == invoice)
|
||||
.where(PaymentEntryReference.allocated_amount < 0)
|
||||
)
|
||||
|
||||
payment_entries = query.run(pluck=True)
|
||||
|
||||
links_to_pe = []
|
||||
if payment_entries:
|
||||
for payment in payment_entries:
|
||||
payment_entry = frappe.get_doc("Payment Entry", payment)
|
||||
if len(payment_entry.references) > 1:
|
||||
links_to_pe.append(payment_entry.name)
|
||||
|
||||
if links_to_pe:
|
||||
payment_entries_link = [
|
||||
get_link_to_form("Payment Entry", name, label=name) for name in links_to_pe
|
||||
|
||||
@@ -27,6 +27,15 @@ frappe.listview_settings["Sales Invoice"] = {
|
||||
"Partly Paid": "yellow",
|
||||
"Internal Transfer": "darkgrey",
|
||||
};
|
||||
|
||||
if (doc.status === "Credit Note Issued" && flt(doc.outstanding_amount) === 0) {
|
||||
return [
|
||||
__("Settled with Credit Note"),
|
||||
"green",
|
||||
`status,=,Credit Note Issued|outstanding_amount,=,0`,
|
||||
];
|
||||
}
|
||||
|
||||
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
right_column: "grand_total",
|
||||
|
||||
@@ -383,6 +383,262 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(si.net_total, 3859.65)
|
||||
self.assertEqual(si.grand_total, 4900.00)
|
||||
|
||||
@ERPNextTestSuite.change_settings("System Settings", {"number_format": "#,###", "currency_precision": 0})
|
||||
def test_inclusive_tax_zero_decimal_currency(self):
|
||||
"""Tax-included prices in zero-decimal currencies (e.g. JPY) must not produce
|
||||
net + tax != gross due to double rounding of the net amount."""
|
||||
si = create_sales_invoice(qty=1, rate=50000, do_not_save=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Tax 10%",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
# With currency_precision=0 (like JPY, KRW):
|
||||
# 50,000 / 1.10 = 45,454.545... → net rounds to 45,455
|
||||
# Tax from unrounded net: 0.10 * 45,454.545 = 4,545.4545 → rounds to 4,545
|
||||
# The fix ensures net + tax = gross without double rounding error
|
||||
self.assertEqual(si.items[0].net_amount, 45455)
|
||||
self.assertEqual(si.taxes[0].tax_amount, 4545)
|
||||
self.assertEqual(si.grand_total, 50000)
|
||||
|
||||
def test_inclusive_tax_decimal_value_currency(self):
|
||||
"""Tax-included prices with decimal currency values must preserve gross total."""
|
||||
si = create_sales_invoice(qty=1, rate=10000.04, do_not_save=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Tax 10%",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
# 10,000.04 / 1.10 = 9,090.94545... → net rounds to 9,090.95
|
||||
# Tax from unrounded net: 0.10 * 9,090.94545... = 909.0945... → rounds to 909.09
|
||||
# If tax were calculated from rounded net instead, it would become 909.10 and grand total 10,000.05.
|
||||
self.assertEqual(si.items[0].net_amount, 9090.95)
|
||||
self.assertEqual(si.taxes[0].tax_amount, 909.09)
|
||||
self.assertEqual(si.grand_total, 10000.04)
|
||||
|
||||
@ERPNextTestSuite.change_settings("System Settings", {"number_format": "#,###", "currency_precision": 0})
|
||||
def test_inclusive_tax_zero_decimal_currency_multiple_items(self):
|
||||
"""Multiple items with tax-included prices in zero-decimal currency."""
|
||||
si = create_sales_invoice(qty=1, rate=50000, do_not_save=True)
|
||||
create_item("_Test Inclusive Tax Item 2")
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Inclusive Tax Item 2",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1,
|
||||
"rate": 30000,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Tax 10%",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
# With currency_precision=0:
|
||||
# Item 1: 50,000 / 1.10 = 45,454.545 → net 45,455, tax 4,545
|
||||
# Item 2: 30,000 / 1.10 = 27,272.727 → net 27,273, tax 2,727
|
||||
# Per-item: net + tax = gross holds (45455+4545=50000, 27273+2727=30000)
|
||||
# Accumulated tax rounds separately: flt(7272.72, 0) = 7273
|
||||
# adjust_grand_total_for_inclusive_tax patches grand_total back to 80000
|
||||
self.assertEqual(si.items[0].net_amount, 45455)
|
||||
self.assertEqual(si.items[1].net_amount, 27273)
|
||||
self.assertEqual(si.net_total, 72728)
|
||||
self.assertEqual(si.taxes[0].tax_amount, 7273)
|
||||
self.assertEqual(si.grand_total, 80000)
|
||||
|
||||
@ERPNextTestSuite.change_settings("System Settings", {"number_format": "#,###", "currency_precision": 0})
|
||||
def test_inclusive_tax_zero_decimal_currency_many_items(self):
|
||||
"""Test with 10 items (mixed 10% and 5% tax) to verify tolerance of 1 is sufficient."""
|
||||
si = create_sales_invoice(qty=1, rate=50000, do_not_save=True)
|
||||
|
||||
# Add 9 more items - mix of amounts and tax rates
|
||||
# Using similar amounts to maximize same-direction rounding
|
||||
item_configs = [
|
||||
("_Test Inclusive Tax Item 2", 50100, None), # 10% (default)
|
||||
("_Test Inclusive Tax Item 3", 50200, '{"_Test Account Service Tax - _TC": 5}'), # 5%
|
||||
("_Test Inclusive Tax Item 4", 50300, None), # 10%
|
||||
("_Test Inclusive Tax Item 5", 50400, '{"_Test Account Service Tax - _TC": 5}'), # 5%
|
||||
("_Test Inclusive Tax Item 6", 50500, None), # 10%
|
||||
("_Test Inclusive Tax Item 7", 50600, '{"_Test Account Service Tax - _TC": 5}'), # 5%
|
||||
("_Test Inclusive Tax Item 8", 50700, None), # 10%
|
||||
("_Test Inclusive Tax Item 9", 50800, None), # 10%
|
||||
("_Test Inclusive Tax Item 10", 50900, '{"_Test Account Service Tax - _TC": 5}'), # 5%
|
||||
]
|
||||
|
||||
for item_code, rate, item_tax_rate in item_configs:
|
||||
create_item(item_code)
|
||||
item_dict = {
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1,
|
||||
"rate": rate,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
if item_tax_rate:
|
||||
item_dict["item_tax_rate"] = item_tax_rate
|
||||
si.append("items", item_dict)
|
||||
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Tax 10%",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
# Verify each item: net + tax = gross (within rounding tolerance)
|
||||
total_gross = 0
|
||||
for item in si.items:
|
||||
total_gross += item.amount
|
||||
|
||||
# Grand total should match sum of gross amounts
|
||||
# This tests that the tolerance of 1 handles mixed tax rates and similar amounts
|
||||
self.assertEqual(si.grand_total, total_gross)
|
||||
|
||||
def test_inclusive_tax_with_decimal_value_on_previous_row_amount(self):
|
||||
"""Inclusive tax with decimal value and On Previous Row Amount must not double-round net amount."""
|
||||
si = create_sales_invoice(qty=1, rate=50000.55, do_not_save=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Tax 10%",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"account_head": "_Test Account Education Cess - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Cess 5% on Tax 10%",
|
||||
"rate": 5,
|
||||
"row_id": 1,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
# Tax fractions: 10% + (5% of 10%) = 10.5%
|
||||
# 50,000.55 / 1.105 = 45,249.3665... → net rounds to 45,249.37
|
||||
# Taxes are calculated from the unrounded net to keep the inclusive gross stable.
|
||||
self.assertEqual(si.items[0].net_amount, 45249.37)
|
||||
self.assertEqual(si.taxes[0].tax_amount, 4524.94)
|
||||
self.assertEqual(si.taxes[1].tax_amount, 226.25)
|
||||
self.assertEqual(si.grand_total, 50000.55)
|
||||
|
||||
def test_inclusive_tax_with_decimal_value_on_previous_row_amount_non_inclusive(self):
|
||||
"""Non-inclusive previous-row tax should be added after inclusive tax extraction."""
|
||||
si = create_sales_invoice(qty=1, rate=10000.04, do_not_save=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Tax 10%",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Previous Row Amount",
|
||||
"account_head": "_Test Account Education Cess - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Cess 5% on Tax 10%",
|
||||
"rate": 5,
|
||||
"row_id": 1,
|
||||
"included_in_print_rate": 0,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
# Only the first tax is inclusive:
|
||||
# 10,000.04 / 1.10 = 9,090.94545... → net rounds to 9,090.95
|
||||
# Inclusive tax = 909.09, restoring the original gross of 10,000.04
|
||||
# The non-inclusive previous-row tax is added afterward: 5% of 909.09 = 45.45
|
||||
self.assertEqual(si.items[0].net_amount, 9090.95)
|
||||
self.assertEqual(si.taxes[0].tax_amount, 909.09)
|
||||
self.assertEqual(si.taxes[1].tax_amount, 45.45)
|
||||
self.assertEqual(si.grand_total, 10045.49)
|
||||
|
||||
def test_inclusive_tax_with_decimal_value_on_previous_row_total(self):
|
||||
"""Inclusive tax with decimal value and On Previous Row Total must not double-round net amount."""
|
||||
si = create_sales_invoice(qty=1, rate=50000.55, do_not_save=True)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Tax 10%",
|
||||
"rate": 10,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Previous Row Total",
|
||||
"account_head": "_Test Account Education Cess - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Cess 5% on Previous Total",
|
||||
"rate": 5,
|
||||
"row_id": 1,
|
||||
"included_in_print_rate": 1,
|
||||
},
|
||||
)
|
||||
si.insert()
|
||||
|
||||
# Tax fractions: 10% + (5% of 110%) = 15.5%
|
||||
# 50,000.55 / 1.155 = 43,290.5195... → net rounds to 43,290.52
|
||||
# Taxes are calculated from the unrounded net/previous total to keep the inclusive gross stable.
|
||||
self.assertEqual(si.items[0].net_amount, 43290.52)
|
||||
self.assertEqual(si.taxes[0].tax_amount, 4329.05)
|
||||
self.assertEqual(si.taxes[1].tax_amount, 2380.98)
|
||||
self.assertEqual(si.grand_total, 50000.55)
|
||||
|
||||
def test_sales_invoice_discount_amount(self):
|
||||
si = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][3])
|
||||
si.discount_amount = 104.94
|
||||
@@ -881,7 +1137,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
link_doctypes = [d.parent for d in link_data]
|
||||
|
||||
# test case for dynamic link order
|
||||
self.assertTrue(link_doctypes.index("GL Entry") > link_doctypes.index("Journal Entry Account"))
|
||||
self.assertGreater(link_doctypes.index("GL Entry"), link_doctypes.index("Journal Entry Account"))
|
||||
|
||||
jv.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0)
|
||||
@@ -2662,6 +2918,34 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(target_doc.company, "_Test Company 1")
|
||||
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
||||
|
||||
def test_inter_company_transaction_does_not_inherit_party_fields(self):
|
||||
"""
|
||||
Party-derived fields on SI (from Customer) must not leak into the mapped PI.
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
debit_to="Debtors - WP",
|
||||
warehouse="Stores - WP",
|
||||
income_account="Sales - WP",
|
||||
expense_account="Cost of Goods Sold - WP",
|
||||
cost_center="Main - WP",
|
||||
currency="USD",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.tax_category = "_Test Tax Category 1"
|
||||
si.language = "ar"
|
||||
si.payment_terms_template = "_Test Payment Term Template"
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
supplier = frappe.get_doc("Supplier", "_Test Internal Supplier")
|
||||
self.assertEqual(pi.tax_category or None, supplier.tax_category or None)
|
||||
self.assertEqual(pi.language or None, supplier.language or None)
|
||||
self.assertEqual(pi.payment_terms_template or None, supplier.payment_terms or None)
|
||||
|
||||
def test_inter_company_transaction_without_default_warehouse(self):
|
||||
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
|
||||
# setup
|
||||
@@ -3319,6 +3603,52 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_sales_invoice_return_common_party_je_has_no_negative_amounts(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
customer = make_customer(customer="_Test Common Party Return SI")
|
||||
supplier = create_supplier(supplier_name="_Test Common Party Return SI").name
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC")
|
||||
|
||||
return_si = make_return_doc(si.doctype, si.name)
|
||||
return_si.submit()
|
||||
|
||||
# JE for the return should credit the supplier (primary/advance) account
|
||||
# and debit the customer (secondary/reconciliation) account — all positive amounts
|
||||
jv_accounts = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": return_si.doctype, "reference_name": return_si.name, "docstatus": 1},
|
||||
fields=["debit_in_account_currency", "credit_in_account_currency", "account"],
|
||||
)
|
||||
|
||||
self.assertTrue(jv_accounts, "Expected a Journal Entry for the return invoice")
|
||||
for row in jv_accounts:
|
||||
self.assertGreaterEqual(
|
||||
row.debit_in_account_currency,
|
||||
0,
|
||||
f"Negative debit on account {row.account}",
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
row.credit_in_account_currency,
|
||||
0,
|
||||
f"Negative credit on account {row.account}",
|
||||
)
|
||||
|
||||
# Customer (secondary) account must be debited, not credited
|
||||
customer_row = next(r for r in jv_accounts if r.account == return_si.debit_to)
|
||||
self.assertGreater(customer_row.debit_in_account_currency, 0)
|
||||
self.assertEqual(customer_row.credit_in_account_currency, 0)
|
||||
|
||||
party_link.delete()
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -3471,7 +3801,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.save()
|
||||
|
||||
self.assertTrue("cannot overbill" in str(err.exception).lower())
|
||||
self.assertIn("cannot overbill", str(err.exception).lower())
|
||||
dn.cancel()
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
@@ -3584,9 +3914,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
si.submit()
|
||||
|
||||
self.assertTrue(
|
||||
"Cannot create accounting entries against disabled accounts" in str(err.exception)
|
||||
)
|
||||
self.assertIn("Cannot create accounting entries against disabled accounts", str(err.exception))
|
||||
|
||||
finally:
|
||||
account.disabled = 0
|
||||
@@ -3681,7 +4009,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
return_si = make_return_doc(si.doctype, si.name)
|
||||
return_si.save().submit()
|
||||
|
||||
self.assertTrue(return_si.docstatus == 1)
|
||||
self.assertEqual(return_si.docstatus, 1)
|
||||
|
||||
def test_sales_invoice_with_payable_tax_account(self):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
@@ -947,7 +947,8 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "available_quantity_section",
|
||||
@@ -1016,7 +1017,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-24 14:37:16.853941",
|
||||
"modified": "2026-05-29 12:23:28.259905",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.utils import cstr
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
|
||||
from erpnext.setup.doctype.supplier_group.supplier_group import get_parent_supplier_groups
|
||||
|
||||
|
||||
class IncorrectCustomerGroup(frappe.ValidationError):
|
||||
@@ -176,38 +177,44 @@ def get_party_details(party: str | None, party_type: str, args: dict | None = No
|
||||
def get_tax_template(posting_date, args):
|
||||
"""Get matching tax rule"""
|
||||
args = frappe._dict(args)
|
||||
conditions = []
|
||||
|
||||
TaxRule = DocType("Tax Rule")
|
||||
query = frappe.qb.from_(TaxRule).select("*")
|
||||
|
||||
if posting_date:
|
||||
conditions.append(
|
||||
f"""(from_date is null or from_date <= '{posting_date}')
|
||||
and (to_date is null or to_date >= '{posting_date}')"""
|
||||
query = query.where(
|
||||
(TaxRule.from_date.isnull() | (TaxRule.from_date <= posting_date))
|
||||
& (TaxRule.to_date.isnull() | (TaxRule.to_date >= posting_date))
|
||||
)
|
||||
else:
|
||||
conditions.append("(from_date is null) and (to_date is null)")
|
||||
query = query.where(TaxRule.from_date.isnull() & TaxRule.to_date.isnull())
|
||||
|
||||
conditions.append(
|
||||
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False))
|
||||
)
|
||||
if "tax_category" in args.keys():
|
||||
del args["tax_category"]
|
||||
def get_group_ancestors(doctype, get_parents, value):
|
||||
if not value:
|
||||
value = get_root_of(doctype)
|
||||
return [""] + [d.name for d in get_parents(value)]
|
||||
|
||||
group_fields = {
|
||||
"customer_group": ("Customer Group", get_parent_customer_groups),
|
||||
"supplier_group": ("Supplier Group", get_parent_supplier_groups),
|
||||
}
|
||||
|
||||
args.setdefault("tax_category", "")
|
||||
|
||||
for key, value in args.items():
|
||||
if key == "use_for_shopping_cart":
|
||||
conditions.append(f"use_for_shopping_cart = {1 if value else 0}")
|
||||
elif key == "customer_group":
|
||||
if not value:
|
||||
value = get_root_of("Customer Group")
|
||||
customer_group_condition = get_customer_group_condition(value)
|
||||
conditions.append(f"ifnull({key}, '') in ('', {customer_group_condition})")
|
||||
query = query.where(TaxRule.use_for_shopping_cart == value)
|
||||
elif key == "tax_category":
|
||||
query = query.where(IfNull(TaxRule.tax_category, "") == (value or ""))
|
||||
elif key in group_fields:
|
||||
doctype, get_parents = group_fields[key]
|
||||
query = query.where(
|
||||
IfNull(TaxRule[key], "").isin(get_group_ancestors(doctype, get_parents, value))
|
||||
)
|
||||
else:
|
||||
conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})")
|
||||
query = query.where(IfNull(TaxRule[key], "").isin(["", value or ""]))
|
||||
|
||||
tax_rule = frappe.db.sql(
|
||||
"""select * from `tabTax Rule`
|
||||
where {}""".format(" and ".join(conditions)),
|
||||
as_dict=True,
|
||||
)
|
||||
tax_rule = query.run(as_dict=True)
|
||||
|
||||
if not tax_rule:
|
||||
return None
|
||||
@@ -236,11 +243,3 @@ def get_tax_template(posting_date, args):
|
||||
return None
|
||||
|
||||
return tax_template
|
||||
|
||||
|
||||
def get_customer_group_condition(customer_group):
|
||||
condition = ""
|
||||
customer_groups = ["%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)]
|
||||
if customer_groups:
|
||||
condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups))
|
||||
return condition
|
||||
|
||||
@@ -61,6 +61,117 @@ class TestTaxRule(ERPNextTestSuite):
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
def test_for_parent_supplier_group(self):
|
||||
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
|
||||
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Taxes and Charges Template",
|
||||
"title": "_Test Purchase Taxes and Charges Template",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"rate": 6,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
make_tax_rule(
|
||||
supplier_group="All Supplier Groups",
|
||||
tax_type="Purchase",
|
||||
purchase_tax_template=purchase_template,
|
||||
priority=1,
|
||||
use_for_shopping_cart=0,
|
||||
from_date="2015-01-01",
|
||||
save=1,
|
||||
)
|
||||
|
||||
# "_Test Supplier Group" has "All Supplier Groups" as its parent — should match hierarchically
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"tax_type": "Purchase",
|
||||
"use_for_shopping_cart": 0,
|
||||
},
|
||||
),
|
||||
purchase_template,
|
||||
)
|
||||
|
||||
def test_use_for_shopping_cart_filter(self):
|
||||
city = "Test Cart City"
|
||||
# higher priority ensures this rule wins when use_for_shopping_cart is not filtered
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
|
||||
use_for_shopping_cart=0,
|
||||
priority=2,
|
||||
save=1,
|
||||
)
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
|
||||
use_for_shopping_cart=1,
|
||||
priority=1,
|
||||
save=1,
|
||||
)
|
||||
|
||||
# Cart request (use_for_shopping_cart=1) filters to cart rules only
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template 1 - _TC",
|
||||
)
|
||||
|
||||
# Non-cart request omits use_for_shopping_cart — no filter is applied, both rules
|
||||
# are candidates; non-cart rule wins by higher priority
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
def test_use_for_shopping_cart_default(self):
|
||||
city = "Test Default Cart City"
|
||||
# use_for_shopping_cart not set — Check field defaults to 0
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
|
||||
use_for_shopping_cart=0, # Default is set to 1.
|
||||
save=1,
|
||||
)
|
||||
|
||||
# Non-cart request (no use_for_shopping_cart in args) matches the rule
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
# Cart request (use_for_shopping_cart=1) does not match — rule has default 0
|
||||
self.assertIsNone(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
|
||||
)
|
||||
)
|
||||
|
||||
def test_conflict_with_overlapping_dates(self):
|
||||
tax_rule1 = make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
@@ -276,7 +387,7 @@ class TestTaxRule(ERPNextTestSuite):
|
||||
self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC")
|
||||
|
||||
# Check if accounts heads and rate fetched are also fetched from tax template or not
|
||||
self.assertTrue(len(quotation.taxes) > 0)
|
||||
self.assertGreater(len(quotation.taxes), 0)
|
||||
|
||||
|
||||
def make_tax_rule(**args):
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils import cstr, getdate
|
||||
|
||||
from erpnext import allow_regional
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
@@ -48,7 +48,7 @@ class TaxWithholdingCategory(Document):
|
||||
for d in self.get("rates"):
|
||||
if getdate(d.from_date) >= getdate(d.to_date):
|
||||
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
||||
group_rates[d.tax_withholding_group].append(d)
|
||||
group_rates[cstr(d.tax_withholding_group)].append(d)
|
||||
|
||||
# Validate overlapping dates within each group
|
||||
for group, rates in group_rates.items():
|
||||
@@ -92,10 +92,9 @@ class TaxWithholdingCategory(Document):
|
||||
|
||||
def get_applicable_tax_row(self, posting_date, tax_withholding_group):
|
||||
for row in self.rates:
|
||||
if (
|
||||
getdate(row.from_date) <= getdate(posting_date) <= getdate(row.to_date)
|
||||
and row.tax_withholding_group == tax_withholding_group
|
||||
):
|
||||
if getdate(row.from_date) <= getdate(posting_date) <= getdate(row.to_date) and cstr(
|
||||
row.tax_withholding_group
|
||||
) == cstr(tax_withholding_group):
|
||||
return row
|
||||
|
||||
frappe.throw(_("No Tax Withholding data found for the current posting date."))
|
||||
@@ -116,7 +115,7 @@ class TaxWithholdingDetails:
|
||||
def __init__(
|
||||
self,
|
||||
tax_withholding_categories: list[str],
|
||||
tax_withholding_group: str,
|
||||
tax_withholding_group: str | None,
|
||||
posting_date: str,
|
||||
party_type: str,
|
||||
party: str,
|
||||
|
||||
@@ -476,7 +476,7 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
|
||||
# Cumulative threshold is 10,000
|
||||
# Threshold calculation should be only on the third invoice
|
||||
self.assertTrue(len(pi1.taxes) > 0)
|
||||
self.assertGreater(len(pi1.taxes), 0)
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
self.cleanup_invoices(invoices)
|
||||
@@ -999,6 +999,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
|
||||
self.cleanup_invoices(invoices)
|
||||
|
||||
def test_null_and_empty_tax_withholding_group_are_equivalent(self):
|
||||
"""
|
||||
NULL and empty-string `tax_withholding_group` must be treated as the
|
||||
same value.
|
||||
"""
|
||||
category = frappe.get_doc("Tax Withholding Category", "Cumulative Threshold TDS")
|
||||
original_row = category.rates[0]
|
||||
original_row.tax_withholding_group = None
|
||||
|
||||
# Part 1: validate_dates must detect overlap between NULL-group and
|
||||
# empty-string-group rows covering the same date range.
|
||||
category.append(
|
||||
"rates",
|
||||
{
|
||||
"from_date": original_row.from_date,
|
||||
"to_date": original_row.to_date,
|
||||
"tax_withholding_group": "",
|
||||
"tax_withholding_rate": original_row.tax_withholding_rate,
|
||||
},
|
||||
)
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
category.validate_dates()
|
||||
category.rates.pop()
|
||||
|
||||
# Part 2: get_applicable_tax_row must match NULL <-> "" in either direction.
|
||||
posting_date = original_row.from_date
|
||||
|
||||
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group="")
|
||||
self.assertEqual(row.name, original_row.name)
|
||||
|
||||
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group=None)
|
||||
self.assertEqual(row.name, original_row.name)
|
||||
|
||||
original_row.tax_withholding_group = ""
|
||||
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group=None)
|
||||
self.assertEqual(row.name, original_row.name)
|
||||
|
||||
original_row.tax_withholding_group = None
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group="194R")
|
||||
|
||||
def test_tds_calculation_on_net_total(self):
|
||||
self.setup_party_with_category("Supplier", "Test TDS Supplier4", "Cumulative Threshold TDS")
|
||||
invoices = []
|
||||
@@ -3613,7 +3654,7 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=50000, do_not_save=True)
|
||||
pi.save()
|
||||
|
||||
self.assertTrue(len(pi.tax_withholding_entries) > 0)
|
||||
self.assertGreater(len(pi.tax_withholding_entries), 0)
|
||||
pi.delete()
|
||||
|
||||
def test_tds_rounding_with_decimal_amounts(self):
|
||||
@@ -3679,7 +3720,7 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
self.setup_party_with_category("Supplier", "Test TDS Supplier", "Cumulative Threshold TDS")
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=50000)
|
||||
|
||||
self.assertTrue(len(pi.tax_withholding_entries) > 0)
|
||||
self.assertGreater(len(pi.tax_withholding_entries), 0)
|
||||
pi.override_tax_withholding_entries = 1
|
||||
|
||||
entry = pi.tax_withholding_entries[0]
|
||||
|
||||
@@ -430,7 +430,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
gle.flags.adv_adj = adv_adj
|
||||
gle.flags.update_outstanding = update_outstanding or "Yes"
|
||||
gle.flags.notify_update = False
|
||||
if gle.is_cancelled:
|
||||
if gle.is_cancelled or is_immutable_ledger_enabled():
|
||||
gle.flags.ignore_links = True
|
||||
gle.submit()
|
||||
|
||||
@@ -718,7 +718,12 @@ def make_reverse_gl_entries(
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
|
||||
|
||||
# For reverse entries, use the posting_date parameter if provided and valid
|
||||
# Otherwise fall back to original posting_date
|
||||
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
|
||||
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
|
||||
|
||||
if partial_cancel:
|
||||
# Partial cancel is only used by `Advance` in separate account feature.
|
||||
# Only cancel GL entries for unlinked reference using `voucher_detail_no`
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Invoice",
|
||||
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
|
||||
"dynamic_filters_json": "[[\"Purchase Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Bills",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Bills",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Payment",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Payment",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Invoice",
|
||||
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
|
||||
"dynamic_filters_json": "[[\"Sales Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Outgoing Bills",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Outgoing Bills",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Outgoing Payment",
|
||||
"modified": "2024-12-05 12:00:00.000000",
|
||||
"modified": "2026-06-01 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Outgoing Payment",
|
||||
|
||||
@@ -49,6 +49,25 @@ SALES_TRANSACTION_TYPES = {
|
||||
}
|
||||
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
|
||||
|
||||
# Party-derived fields that must NOT be auto-copied by `get_mapped_doc` when the
|
||||
# source and target documents belong to different parties (e.g. Sales Order →
|
||||
# Purchase Order or inter-company Sales Invoice → Purchase Invoice).
|
||||
CROSS_PARTY_FIELD_NO_MAP = [
|
||||
"tax_category",
|
||||
"tax_id",
|
||||
"tax_withholding_category",
|
||||
"taxes_and_charges",
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
"payment_terms_template",
|
||||
"language",
|
||||
]
|
||||
|
||||
|
||||
class DuplicatePartyAccountError(frappe.ValidationError):
|
||||
pass
|
||||
@@ -65,7 +84,6 @@ def get_party_details(
|
||||
price_list: str | None = None,
|
||||
currency: str | None = None,
|
||||
doctype: str | None = None,
|
||||
ignore_permissions: bool | None = False,
|
||||
fetch_payment_terms_template: bool = True,
|
||||
party_address: str | None = None,
|
||||
company_address: str | None = None,
|
||||
@@ -75,8 +93,6 @@ def get_party_details(
|
||||
):
|
||||
if not party:
|
||||
return frappe._dict()
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
|
||||
return _get_party_details(
|
||||
party,
|
||||
account,
|
||||
@@ -87,7 +103,7 @@ def get_party_details(
|
||||
price_list,
|
||||
currency,
|
||||
doctype,
|
||||
ignore_permissions,
|
||||
False,
|
||||
fetch_payment_terms_template,
|
||||
party_address,
|
||||
company_address,
|
||||
@@ -687,7 +703,7 @@ def validate_due_date_with_template(posting_date, due_date, bill_date, template_
|
||||
if not default_due_date:
|
||||
return
|
||||
|
||||
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
|
||||
if getdate(default_due_date) != getdate(posting_date) and getdate(due_date) > getdate(default_due_date):
|
||||
if frappe.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
|
||||
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
|
||||
|
||||
@@ -762,7 +778,7 @@ def set_taxes(
|
||||
args.update({"tax_type": "Purchase"})
|
||||
|
||||
if use_for_shopping_cart:
|
||||
args.update({"use_for_shopping_cart": use_for_shopping_cart})
|
||||
args.update({"use_for_shopping_cart": cint(use_for_shopping_cart)})
|
||||
|
||||
return get_tax_template(posting_date, args)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,161 +0,0 @@
|
||||
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
|
||||
{% if letter_head and not no_letterhead %}
|
||||
<div class="letter-head">{{ letter_head }}</div>
|
||||
{% endif %}
|
||||
{% if print_heading_template %}
|
||||
{{ frappe.render_template(print_heading_template, {"doc":doc}) }}
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{%- if doc.meta.is_submittable and doc.docstatus==2-%}
|
||||
<div class="text-center" document-status="cancelled">
|
||||
<h4 style="margin: 0px;">{{ _("CANCELLED") }}</h4>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
{% for page in layout %}
|
||||
<div class="page-break">
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}
|
||||
</div>
|
||||
<style>
|
||||
.taxes-section .order-taxes.mt-5{
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
.taxes-section .order-taxes .border-btm.pb-5{
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
.print-format label{
|
||||
color: #74808b;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if not no_letterhead and footer %}
|
||||
<div class="letter-head-footer">
|
||||
{{ footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-center small page-number visible-pdf">
|
||||
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row section-break" style="margin-bottom: 10px;">
|
||||
<div class="col-xs-6 p-0">
|
||||
<div class="col-xs-12 value text-uppercase"><b>{{ doc.customer }}</b></div>
|
||||
<div class="col-xs-12">
|
||||
{{ doc.address_display }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ _("Contact: ")+doc.contact_display if doc.contact_display else '' }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ _("Mobile: ")+doc.contact_mobile if doc.contact_mobile else '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-3"></div>
|
||||
<div class="col-xs-3" style="padding-left: 5px;">
|
||||
<div>
|
||||
<div><label>{{ _("Invoice ID") }}</label></div>
|
||||
<div>{{ doc.name }}</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div><label>{{ _("Invoice Date") }}</label></div>
|
||||
<div>{{ frappe.utils.format_date(doc.posting_date) }}</div>
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<div><label>{{ _("Due Date") }}</label></div>
|
||||
<div>{{ frappe.utils.format_date(doc.due_date) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-break">
|
||||
<table class="table table-bordered table-condensed mb-0" style="width: 100%; border-collapse: collapse; font-size: 12px;">
|
||||
<colgroup>
|
||||
<col style="width: 5%">
|
||||
<col style="width: 45%">
|
||||
<col style="width: 10%">
|
||||
<col style="width: 20%">
|
||||
<col style="width: 20%">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Sr") }}</th>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Details") }}</th>
|
||||
<th class="text-uppercase" style="text-align:center">{{ _("Qty") }}</th>
|
||||
<th class="text-uppercase" style="text-align:right">{{ _("Rate") }}</th>
|
||||
<th class="text-uppercase" style="text-align:right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for item in doc.items %}
|
||||
<tr>
|
||||
<td style="text-align:center">{{ loop.index }}</td>
|
||||
<td>
|
||||
<b>{{ item.item_code }}: {{ item.item_name }}</b>
|
||||
{% if (item.description != item.item_name) %}
|
||||
<br>{{ item.description }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
{{ item.get_formatted("qty", 0) }}
|
||||
{{ item.get_formatted("uom", 0) }}
|
||||
</td>
|
||||
<td style="text-align: right;">{{ item.get_formatted("net_rate", doc) }}</td>
|
||||
<td style="text-align: right;">{{ item.get_formatted("net_amount", doc) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<!-- total -->
|
||||
<div class="row">
|
||||
|
||||
<div class="col-xs-6">
|
||||
<div>
|
||||
<label>{{ _("Amount in Words") }}</label>
|
||||
{{ doc.in_words }}
|
||||
</div>
|
||||
<div style="margin-top: 20px;">
|
||||
<label>{{ _("Payment Status") }}</label>
|
||||
{{ doc.status }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<div class="row section-break">
|
||||
<div class="col-xs-7"><div>{{ _("Sub Total") }}</div></div>
|
||||
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("net_total", doc) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% for d in doc.taxes %}
|
||||
{% if d.tax_amount %}
|
||||
<div class="row">
|
||||
<div class="col-xs-8"><div>{{ _(d.description) }}</div></div>
|
||||
<div class="col-xs-4" style="text-align: right;">{{ d.get_formatted("tax_amount") }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-7"><div>{{ _("Total") }}</div></div>
|
||||
<div class="col-xs-5" style="text-align: right;">{{ doc.get_formatted("grand_total", doc) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row important data-field">
|
||||
<div class="col-xs-12"><label>{{ _("Terms and Conditions") }}: </label></div>
|
||||
<div class="col-xs-12">{{ doc.terms if doc.terms else '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"absolute_value": 0,
|
||||
"align_labels_right": 0,
|
||||
"creation": "2025-01-22 16:23:51.012200",
|
||||
"css": "",
|
||||
"custom_format": 0,
|
||||
"default_print_language": "en",
|
||||
"disabled": 0,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "",
|
||||
"font_size": 14,
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"margin_bottom": 0.0,
|
||||
"margin_left": 0.0,
|
||||
"margin_right": 0.0,
|
||||
"margin_top": 0.0,
|
||||
"modified": "2025-01-22 16:23:51.012200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Print",
|
||||
"owner": "Administrator",
|
||||
"page_number": "Hide",
|
||||
"print_format_builder": 0,
|
||||
"print_format_builder_beta": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-04-22 16:16:03",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:09:46.150861",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Payable",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Accounts Payable",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-04-22 16:16:03",
|
||||
"default_print_format": "Accounts Payable Standard",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-05-22 14:35:14.716933",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Payable",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Accounts Payable",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2014-11-04 12:09:59.672379",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:11:35.655834",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Payable Summary",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Accounts Payable Summary",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2014-11-04 12:09:59.672379",
|
||||
"default_print_format": "Accounts Payable Summary Standard",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-05-22 14:35:19.179799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Payable Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Accounts Payable Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-04-16 11:31:13",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-03-06 05:52:06.235584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Receivable",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Accounts Receivable",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-04-16 11:31:13",
|
||||
"default_print_format": "Accounts Receivable Standard",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 5,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-05-22 14:34:57.666402",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Receivable",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Accounts Receivable",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.database.schema import get_definition
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -131,8 +130,6 @@ class ReceivablePayableReport:
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
elif self.ple_fetch_method == "Raw SQL":
|
||||
self.fetch_ple_in_sql_procedures()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
@@ -323,81 +320,6 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
def fetch_ple_in_sql_procedures(self):
|
||||
self.proc = InitSQLProceduresForAR()
|
||||
|
||||
build_balance = f"""
|
||||
begin not atomic
|
||||
declare done boolean default false;
|
||||
declare rec1 row type of `{self.proc._row_def_table_name}`;
|
||||
declare ple cursor for {self.ple_query.get_sql()};
|
||||
declare continue handler for not found set done = true;
|
||||
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.init_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
|
||||
set done = false;
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.allocate_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
end;
|
||||
"""
|
||||
frappe.db.sql(build_balance)
|
||||
|
||||
balances = frappe.db.sql(
|
||||
f"""select
|
||||
name,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
party,
|
||||
party_account `account`,
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
project,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
|
||||
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
|
||||
sum(paid_in_account_currency) `paid_in_account_currency`,
|
||||
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
|
||||
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
|
||||
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
|
||||
as_dict=True,
|
||||
)
|
||||
for x in balances:
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (x.voucher_type, x.voucher_no, x.party)
|
||||
else:
|
||||
key = (x.account, x.voucher_type, x.voucher_no, x.party)
|
||||
|
||||
_d = self.build_voucher_dict(x)
|
||||
for field in [
|
||||
"invoiced",
|
||||
"paid",
|
||||
"credit_note",
|
||||
"outstanding",
|
||||
"invoiced_in_account_currency",
|
||||
"paid_in_account_currency",
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
"project",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
self.voucher_balance[key] = _d
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -1410,120 +1332,3 @@ def get_party_group_with_children(party, party_groups):
|
||||
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
|
||||
|
||||
return list(set(all_party_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
"""
|
||||
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
|
||||
"""
|
||||
|
||||
_varchar_type = get_definition("Data")
|
||||
_currency_type = get_definition("Currency")
|
||||
# Temporary Tables
|
||||
_voucher_balance_name = "_ar_voucher_balance"
|
||||
_voucher_balance_definition = f"""
|
||||
create temporary table `{_voucher_balance_name}`(
|
||||
name {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
party {_varchar_type},
|
||||
party_account {_varchar_type},
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
project {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
invoiced_in_account_currency {_currency_type},
|
||||
paid_in_account_currency {_currency_type},
|
||||
credit_note_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
_row_def_table_name = "_ar_ple_row"
|
||||
_row_def_table_definition = f"""
|
||||
create temporary table `{_row_def_table_name}`(
|
||||
name {_varchar_type},
|
||||
account {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
against_voucher_type {_varchar_type},
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
project {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
account_currency {_varchar_type},
|
||||
amount {_currency_type},
|
||||
amount_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
# Procedures
|
||||
init_procedure_name = "ar_init_tmp_table"
|
||||
init_procedure_sql = f"""
|
||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
|
||||
allocate_procedure_name = "ar_allocate_to_tmp_table"
|
||||
allocate_procedure_sql = f"""
|
||||
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
declare invoiced {_currency_type} default 0;
|
||||
declare invoiced_in_account_currency {_currency_type} default 0;
|
||||
declare paid {_currency_type} default 0;
|
||||
declare paid_in_account_currency {_currency_type} default 0;
|
||||
declare credit_note {_currency_type} default 0;
|
||||
declare credit_note_in_account_currency {_currency_type} default 0;
|
||||
|
||||
|
||||
if ple.amount > 0 then
|
||||
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set invoiced = ple.amount;
|
||||
set invoiced_in_account_currency = ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
|
||||
if (ple.voucher_no = ple.against_voucher_no) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set credit_note = -1 * ple.amount;
|
||||
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
existing_procedures = frappe.db.get_routines()
|
||||
|
||||
if self.init_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.init_procedure_sql)
|
||||
|
||||
if self.allocate_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.allocate_procedure_sql)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
|
||||
frappe.db.sql(self._voucher_balance_definition)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
|
||||
frappe.db.sql(self._row_def_table_definition)
|
||||
|
||||
@@ -194,7 +194,7 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
report = execute(filters)
|
||||
|
||||
row = report[1]
|
||||
self.assertTrue(len(row) == 0)
|
||||
self.assertEqual(len(row), 0)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Accounts Settings",
|
||||
@@ -764,7 +764,7 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
report = execute(filters)[1]
|
||||
|
||||
# Assert that the report contains data for the specified customer groups
|
||||
self.assertTrue(len(report) > 0)
|
||||
self.assertGreater(len(report), 0)
|
||||
|
||||
for row in report:
|
||||
# Assert that the customer group of each row is in the list of customer groups
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2014-10-17 15:45:00.694265",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-03-06 05:52:23.751082",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Receivable Summary",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Accounts Receivable Summary",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2014-10-17 15:45:00.694265",
|
||||
"default_print_format": "Accounts Receivable Summary Standard",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-05-22 14:35:10.656797",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Receivable Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Accounts Receivable Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"creation": "2014-07-14 05:24:20.385279",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-09-07 12:18:21.850851",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Balance Sheet",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Balance Sheet",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2014-07-14 05:24:20.385279",
|
||||
"default_print_format": "Balance Sheet Standard",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-05-22 14:35:28.187799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Balance Sheet",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Balance Sheet",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ function get_filters() {
|
||||
let budget_against_options = get_dimensions();
|
||||
|
||||
let filters = [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_fiscal_year",
|
||||
label: __("From Fiscal Year"),
|
||||
@@ -67,14 +75,6 @@ function get_filters() {
|
||||
default: "Yearly",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "budget_against",
|
||||
label: __("Budget Against"),
|
||||
@@ -96,9 +96,12 @@ function get_filters() {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let budget_against = frappe.query_report.get_filter_value("budget_against");
|
||||
let company = frappe.query_report.get_filter_value("company");
|
||||
if (!budget_against) return;
|
||||
|
||||
return frappe.db.get_link_options(budget_against, txt);
|
||||
const filters = budget_against !== "Branch" && company ? { company: company } : {};
|
||||
|
||||
return frappe.db.get_link_options(budget_against, txt, filters);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.utils import add_months, flt, formatdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -19,6 +20,8 @@ def execute(filters=None):
|
||||
columns = get_columns(filters)
|
||||
if filters.get("budget_against_filter"):
|
||||
dimensions = filters.get("budget_against_filter")
|
||||
if filters.get("budget_against") == "Cost Center":
|
||||
dimensions = get_cost_center_with_children(dimensions)
|
||||
else:
|
||||
dimensions = get_budget_dimensions(filters)
|
||||
if not dimensions:
|
||||
@@ -40,39 +43,29 @@ def validate_filters(filters):
|
||||
|
||||
def get_budget_records(filters, dimensions):
|
||||
budget_against_field = frappe.scrub(filters["budget_against"])
|
||||
budget = frappe.qb.DocType("Budget")
|
||||
|
||||
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,
|
||||
)
|
||||
return (
|
||||
frappe.qb.from_(budget)
|
||||
.select(
|
||||
budget.name,
|
||||
budget.account,
|
||||
budget[budget_against_field].as_("dimension"),
|
||||
budget.budget_amount,
|
||||
budget.from_fiscal_year,
|
||||
budget.to_fiscal_year,
|
||||
budget.budget_start_date,
|
||||
budget.budget_end_date,
|
||||
)
|
||||
.where(
|
||||
(budget.company == filters.company)
|
||||
& (budget.docstatus == 1)
|
||||
& (budget.budget_against == filters.budget_against)
|
||||
& (budget[budget_against_field].isin(dimensions))
|
||||
& (budget.from_fiscal_year <= filters.to_fiscal_year)
|
||||
& (budget.to_fiscal_year >= filters.from_fiscal_year)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def build_budget_map(budget_records, filters):
|
||||
@@ -120,50 +113,41 @@ def build_budget_map(budget_records, filters):
|
||||
|
||||
def get_actual_transactions(dimension_name, filters):
|
||||
budget_against = frappe.scrub(filters.get("budget_against"))
|
||||
cost_center_filter = ""
|
||||
monthname = CustomFunction("MONTHNAME", ["date"])
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
budget = frappe.qb.DocType("Budget")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.from_(budget)
|
||||
.select(
|
||||
gle.account,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.fiscal_year,
|
||||
monthname(gle.posting_date).as_("month_name"),
|
||||
budget[budget_against].as_("budget_against"),
|
||||
)
|
||||
.where(
|
||||
(budget.docstatus == 1)
|
||||
& (budget.account == gle.account)
|
||||
& (gle.fiscal_year >= filters.from_fiscal_year)
|
||||
& (gle.fiscal_year <= filters.to_fiscal_year)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (budget[budget_against] == dimension_name)
|
||||
)
|
||||
.groupby(gle.name)
|
||||
.orderby(gle.fiscal_year)
|
||||
)
|
||||
|
||||
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}"
|
||||
"""
|
||||
cost_centers = get_cost_center_with_children([dimension_name])
|
||||
query = query.where(gle.cost_center.isin(cost_centers))
|
||||
else:
|
||||
query = query.where(budget[budget_against] == gle[budget_against])
|
||||
|
||||
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 = query.run(as_dict=True)
|
||||
|
||||
actual_transactions_map = {}
|
||||
for transaction in actual_transactions:
|
||||
@@ -382,33 +366,37 @@ def get_fiscal_years(filters):
|
||||
return fiscal_year
|
||||
|
||||
|
||||
def get_budget_dimensions(filters):
|
||||
order_by = ""
|
||||
if filters.get("budget_against") == "Cost Center":
|
||||
order_by = "order by lft"
|
||||
|
||||
if filters.get("budget_against") in ["Cost Center", "Project"]:
|
||||
return frappe.db.sql_list(
|
||||
"""
|
||||
select
|
||||
name
|
||||
from
|
||||
`tab{tab}`
|
||||
where
|
||||
company = %s
|
||||
{order_by}
|
||||
""".format(tab=filters.get("budget_against"), order_by=order_by),
|
||||
filters.get("company"),
|
||||
def get_cost_center_with_children(cost_centers):
|
||||
"""Expand each cost center to include itself and all its descendants."""
|
||||
cc = frappe.qb.DocType("Cost Center")
|
||||
all_cost_centers = set()
|
||||
for cost_center in cost_centers:
|
||||
result = frappe.db.get_value("Cost Center", cost_center, ["lft", "rgt"])
|
||||
if not result:
|
||||
continue
|
||||
lft, rgt = result
|
||||
children = (
|
||||
frappe.qb.from_(cc).select(cc.name).where((cc.lft >= lft) & (cc.rgt <= rgt)).run(pluck="name")
|
||||
)
|
||||
all_cost_centers.update(children)
|
||||
return list(all_cost_centers)
|
||||
|
||||
|
||||
def get_budget_dimensions(filters):
|
||||
budget_against = filters.get("budget_against")
|
||||
dimension = frappe.qb.DocType(budget_against)
|
||||
|
||||
if budget_against in ["Cost Center", "Project"]:
|
||||
query = (
|
||||
frappe.qb.from_(dimension)
|
||||
.select(dimension.name)
|
||||
.where(dimension.company == filters.get("company"))
|
||||
)
|
||||
if budget_against == "Cost Center":
|
||||
query = query.orderby(dimension.lft)
|
||||
return query.run(pluck="name")
|
||||
else:
|
||||
return frappe.db.sql_list(
|
||||
"""
|
||||
select
|
||||
name
|
||||
from
|
||||
`tab{tab}`
|
||||
""".format(tab=filters.get("budget_against"))
|
||||
) # nosec
|
||||
return frappe.qb.from_(dimension).select(dimension.name).run(pluck="name")
|
||||
|
||||
|
||||
def validate_budget_dimensions(filters):
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2015-12-12 10:22:45.383203",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:09:19.748690",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cash Flow",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Cash Flow",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2015-12-12 10:22:45.383203",
|
||||
"default_print_format": "Cash Flow Statement Standard",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-05-22 14:35:34.353508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cash Flow",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Cash Flow",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -177,10 +177,16 @@ frappe.query_reports["General Ledger"] = {
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "disable_opening_balance_calculation",
|
||||
label: __("Disable Opening Balance Calculation"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_opening_entries",
|
||||
label: __("Show Opening Entries"),
|
||||
fieldtype: "Check",
|
||||
depends_on: "eval: !doc.disable_opening_balance_calculation",
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-12-06 13:22:23",
|
||||
"default_print_format": "General Ledger Standard",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"idx": 4,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 15:47:59.597853",
|
||||
"modified": "2026-05-22 14:34:35.246000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General Ledger",
|
||||
|
||||
@@ -283,7 +283,15 @@ def get_conditions(filters):
|
||||
if filters.get("party"):
|
||||
conditions.append("party in %(party)s")
|
||||
|
||||
if not (
|
||||
if filters.get("disable_opening_balance_calculation"):
|
||||
if not ignore_is_opening:
|
||||
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
|
||||
else:
|
||||
conditions.append("posting_date >=%(from_date)s")
|
||||
|
||||
# opening balance calculation is done only if filtered on account/party
|
||||
# so from_date filter is not applied
|
||||
elif not (
|
||||
filters.get("account")
|
||||
or filters.get("party")
|
||||
or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"]
|
||||
@@ -553,7 +561,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
gle.remarks = _(gle.remarks)
|
||||
gle.party_type = _(gle.party_type)
|
||||
|
||||
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
|
||||
if gle.posting_date < from_date or (
|
||||
cstr(gle.is_opening) == "Yes"
|
||||
and not show_opening_entries
|
||||
and not filters.disable_opening_balance_calculation
|
||||
):
|
||||
if not group_by_voucher_consolidated:
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Gross and Net Profit Report"] = $.extend({}, erpnext.financial_statements);
|
||||
const GNP_REPORT = "Gross and Net Profit Report";
|
||||
|
||||
frappe.query_reports["Gross and Net Profit Report"]["filters"].push({
|
||||
frappe.query_reports[GNP_REPORT] = $.extend({}, erpnext.financial_statements);
|
||||
|
||||
erpnext.utils.add_dimensions(GNP_REPORT, 10);
|
||||
|
||||
frappe.query_reports[GNP_REPORT]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
|
||||
@@ -812,19 +812,11 @@ class GrossProfitGenerator:
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, row.item_row, item_code
|
||||
)
|
||||
elif self.delivery_notes.get((row.parent, row.item_code), None):
|
||||
# check if Invoice has delivery notes
|
||||
dn = self.delivery_notes.get((row.parent, row.item_code))
|
||||
parenttype, parent, item_row, dn_warehouse = (
|
||||
"Delivery Note",
|
||||
dn["delivery_note"],
|
||||
dn["item_row"],
|
||||
dn["warehouse"],
|
||||
)
|
||||
my_sle = self.get_stock_ledger_entries(item_code, dn_warehouse)
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, item_row, item_code
|
||||
)
|
||||
elif row.item_row and self.delivery_notes.get(row.item_row):
|
||||
dn = self.delivery_notes[row.item_row]
|
||||
if flt(dn.total_qty):
|
||||
return flt(row.qty) * flt(dn.total_incoming_value) / flt(dn.total_qty)
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
elif row.sales_order and row.so_detail:
|
||||
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
|
||||
if incoming_amount:
|
||||
@@ -1076,25 +1068,29 @@ class GrossProfitGenerator:
|
||||
def get_delivery_notes(self):
|
||||
self.delivery_notes = frappe._dict({})
|
||||
if self.si_list:
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
invoices = [x.parent for x in self.si_list]
|
||||
dni = qb.DocType("Delivery Note Item")
|
||||
delivery_notes = (
|
||||
qb.from_(dni)
|
||||
.select(
|
||||
dni.against_sales_invoice.as_("sales_invoice"),
|
||||
dni.item_code,
|
||||
dni.warehouse,
|
||||
dni.parent.as_("delivery_note"),
|
||||
dni.name.as_("item_row"),
|
||||
dni.si_detail,
|
||||
Sum(dni.stock_qty * dni.incoming_rate).as_("total_incoming_value"),
|
||||
Sum(dni.stock_qty).as_("total_qty"),
|
||||
)
|
||||
.where((dni.docstatus == 1) & (dni.against_sales_invoice.isin(invoices)))
|
||||
.groupby(dni.against_sales_invoice, dni.item_code)
|
||||
.orderby(dni.creation, order=Order.desc)
|
||||
.where(
|
||||
(dni.docstatus == 1)
|
||||
& (dni.against_sales_invoice.isin(invoices))
|
||||
& (dni.si_detail.isnotnull())
|
||||
& (dni.si_detail != "")
|
||||
)
|
||||
.groupby(dni.si_detail)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for entry in delivery_notes:
|
||||
self.delivery_notes[(entry.sales_invoice, entry.item_code)] = entry
|
||||
self.delivery_notes[entry.si_detail] = entry
|
||||
|
||||
def group_items_by_invoice(self):
|
||||
"""
|
||||
|
||||
@@ -94,6 +94,9 @@ def get_data(filters):
|
||||
def get_sales_details(filters):
|
||||
item_details_map = {}
|
||||
|
||||
if filters["based_on"] not in ("Sales Order", "Sales Invoice"):
|
||||
frappe.throw(_("Invalid value {0} for 'Based On'").format(filters["based_on"]))
|
||||
|
||||
date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date"
|
||||
|
||||
sales_data = frappe.db.sql(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user