Compare commits

..

1 Commits

Author SHA1 Message Date
diptanilsaha
a43c9c9681 Merge branch 'version-15' into 53117_resolve_conflict 2026-03-03 23:03:30 +05:30
448 changed files with 6427 additions and 18860 deletions

View File

@@ -18,19 +18,7 @@ jobs:
cache: pip
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.1
semgrep:
name: semgrep
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: pip
uses: pre-commit/action@v3.0.0
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules

View File

@@ -1,10 +0,0 @@
{
"disabledLabels": [
"conflicts"
],
"context": {
"repos": [
"frappe/frappe"
]
}
}

View File

@@ -50,6 +50,7 @@ repos:
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.116.0"
__version__ = "15.99.1"
def get_default_company(user=None):

View File

@@ -0,0 +1,126 @@
{
"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
}

View File

@@ -517,7 +517,6 @@ def get_account_autoname(account_number, account_name, company):
def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
account.check_permission("write")
if not account:
return
@@ -579,12 +578,10 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
_ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
new_account.check_permission("write")
old_account.check_permission("write")
if not new_account:
throw(_("Account {0} does not exist").format(new))

View File

@@ -34,13 +34,6 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -324,21 +317,13 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Expense Account"
"account_type": "Stock Adjustment"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -355,7 +340,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Depreciation"
"account_type": "Accumulated Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -1,449 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,839 +0,0 @@
{
"name": "Philippines",
"country": "Philippines",
"tree": {
"Asset": {
"account_number": "1000",
"is_group": 1,
"root_type": "Asset",
"Current Assets": {
"account_number": "1001",
"is_group": 1,
"root_type": "Asset",
"Cash": {
"account_number": "1100",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Cash on Hand": {
"account_number": "1101",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
},
"Petty Cash Fund": {
"account_number": "1200",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Petty Cash Fund": {
"account_number": "1201",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
}
}
},
"Bank Accounts": {
"account_number": "1102",
"is_group": 1,
"root_type": "Asset",
"account_type": "Bank"
},
"Advances to Officers & Employees": {
"account_number": "1290",
"is_group": 1,
"root_type": "Asset",
"Advances to Officers & Employees": {
"account_number": "1291",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable Trade": {
"account_number": "1300",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Trade": {
"account_number": "1301",
"is_group": 0,
"root_type": "Asset",
"account_type": "Receivable"
}
},
"Accounts Receivable - Affiliates": {
"account_number": "1310",
"is_group": 1,
"root_type": "Asset",
"Due from Company": {
"account_number": "1311",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable - Others": {
"account_number": "1400",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Others": {
"account_number": "1401",
"is_group": 0,
"root_type": "Asset"
}
},
"Parts, Materials and Supplies": {
"account_number": "1500",
"is_group": 1,
"root_type": "Asset",
"Parts, Materials and Supplies": {
"account_number": "1501",
"is_group": 0,
"root_type": "Asset"
},
"Raw Materials - Demo": {
"account_number": "1502",
"is_group": 0,
"root_type": "Asset"
}
},
"Project in Progress": {
"account_number": "1510",
"is_group": 1,
"root_type": "Asset",
"Project in Progress": {
"account_number": "1511",
"is_group": 0,
"root_type": "Asset"
},
"Factory Overhead Variance": {
"account_number": "1512",
"is_group": 0,
"root_type": "Asset"
}
},
"Finished Goods": {
"account_number": "1520",
"is_group": 1,
"root_type": "Asset",
"Finished Goods Inventory": {
"account_number": "1531",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock"
},
"Inventory in Transit": {
"account_number": "1532",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock Adjustment"
}
},
"Prepayments": {
"account_number": "1600",
"is_group": 1,
"root_type": "Asset",
"Prepaid Insurance & Bonds": {
"account_number": "1601",
"is_group": 0,
"root_type": "Asset"
},
"Prepaid Rent": {
"account_number": "1602",
"is_group": 0,
"root_type": "Asset"
}
},
"VAT Input Tax": {
"account_number": "1610",
"is_group": 1,
"root_type": "Asset",
"VAT Input Tax - Goods": {
"account_number": "1611",
"is_group": 0,
"root_type": "Asset",
"account_type": "Tax"
}
}
},
"Non - Current Assets": {
"account_number": "1002",
"is_group": 1,
"root_type": "Asset",
"Property, Plants And Equipments": {
"account_number": "1700",
"is_group": 1,
"root_type": "Asset",
"Land": {
"account_number": "1701",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Buildings & Improvements": {
"account_number": "1702",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Delivery & Trans Equipment": {
"account_number": "1703",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Furniture & Fixtures": {
"account_number": "1704",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Machinery & Equipment": {
"account_number": "1705",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
}
},
"Accum Depr. - Property, Plants and Equipment": {
"account_number": "1800",
"is_group": 1,
"root_type": "Asset",
"Accumulated Dep Bdgs & Improv": {
"account_number": "1801",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Delivery & Trans": {
"account_number": "1802",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Furniture & Fixture": {
"account_number": "1803",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Depreciation - Machinery & Equipment": {
"account_number": "1804",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
}
}
},
"Other Assets": {
"account_number": "1003",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1900",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1901",
"is_group": 0,
"root_type": "Asset"
}
},
"Miscellaneous Deposits": {
"account_number": "1910",
"is_group": 1,
"root_type": "Asset",
"Miscellaneous Deposits": {
"account_number": "1911",
"is_group": 0,
"root_type": "Asset"
}
},
"Retirement Fund": {
"account_number": "1920",
"is_group": 1,
"root_type": "Asset",
"Retirement Fund": {
"account_number": "1921",
"is_group": 0,
"root_type": "Asset"
}
},
"Investment": {
"account_number": "1930",
"is_group": 1,
"root_type": "Asset",
"Investment": {
"account_number": "1931",
"is_group": 0,
"root_type": "Asset"
}
},
"System Development": {
"account_number": "1940",
"is_group": 1,
"root_type": "Asset",
"System Development": {
"account_number": "1941",
"is_group": 0,
"root_type": "Asset"
}
}
}
},
"Liability": {
"account_number": "2000",
"is_group": 1,
"root_type": "Liability",
"Current Liabilities": {
"account_number": "2001",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable Trade": {
"account_number": "2100",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Trade": {
"account_number": "2101",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Accounts Payable Others": {
"account_number": "2110",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Payroll": {
"account_number": "2111",
"is_group": 0,
"root_type": "Liability"
}
},
"VAT Output Tax": {
"account_number": "2200",
"is_group": 1,
"root_type": "Liability",
"VAT Output Tax": {
"account_number": "2201",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Wages": {
"account_number": "2210",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Wages": {
"account_number": "2211",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Expanded": {
"account_number": "2220",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Expanded": {
"account_number": "2221",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Accruals And Other Current Payables": {
"account_number": "2300",
"is_group": 1,
"root_type": "Liability",
"Stock Received But Not Billed": {
"account_number": "2301",
"is_group": 0,
"root_type": "Liability",
"account_type": "Stock Received But Not Billed"
}
},
"Payable to Government and Other Institutions": {
"account_number": "2400",
"is_group": 1,
"root_type": "Liability",
"SSS Premium Payable": {
"account_number": "2401",
"is_group": 0,
"root_type": "Liability"
},
"SSS Salary Loan Payable": {
"account_number": "2402",
"is_group": 0,
"root_type": "Liability"
},
"PhilHealth Premium": {
"account_number": "2403",
"is_group": 0,
"root_type": "Liability"
},
"Pag-ibig Loan Payable": {
"account_number": "2404",
"is_group": 0,
"root_type": "Liability"
},
"Coop Loans": {
"account_number": "2405",
"is_group": 0,
"root_type": "Liability"
},
"Coop Contributions": {
"account_number": "2406",
"is_group": 0,
"root_type": "Liability"
},
"Canteen": {
"account_number": "2407",
"is_group": 0,
"root_type": "Liability"
},
"AUB Loan Payable": {
"account_number": "2408",
"is_group": 0,
"root_type": "Liability"
},
"HSBC Loan Payable": {
"account_number": "2409",
"is_group": 0,
"root_type": "Liability"
}
},
"Customer Deposits": {
"account_number": "2500",
"is_group": 0,
"root_type": "Liability"
}
},
"Non Current Liabilities": {
"account_number": "2002",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2600",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2601",
"is_group": 0,
"root_type": "Liability"
}
},
"Deferred Income": {
"account_number": "2700",
"is_group": 1,
"root_type": "Liability",
"Deferred Income": {
"account_number": "2701",
"is_group": 0,
"root_type": "Liability"
}
},
"Notes Payable": {
"account_number": "2800",
"is_group": 1,
"root_type": "Liability",
"Notes Payable": {
"account_number": "2801",
"is_group": 0,
"root_type": "Liability"
}
},
"Dividends Payable": {
"account_number": "2900",
"is_group": 0,
"root_type": "Liability"
}
}
},
"Equity": {
"account_number": "3000",
"is_group": 1,
"root_type": "Equity",
"STOCKHOLDER'S EQUITY": {
"account_number": "3001",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3100",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3101",
"is_group": 0,
"root_type": "Equity"
}
},
"Subscription Receivable": {
"account_number": "3200",
"is_group": 1,
"root_type": "Equity",
"Subscription Receivable": {
"account_number": "3201",
"is_group": 0,
"root_type": "Equity"
}
},
"Retained Earnings": {
"account_number": "3300",
"is_group": 1,
"root_type": "Equity",
"Retained Earnings": {
"account_number": "3301",
"is_group": 0,
"root_type": "Equity"
}
},
"Current Year (Profit/Loss)": {
"account_number": "3400",
"is_group": 0,
"root_type": "Equity"
},
"Drawings": {
"account_number": "3500",
"is_group": 1,
"root_type": "Equity",
"Drawings": {
"account_number": "3501",
"is_group": 0,
"root_type": "Equity"
}
}
}
},
"Income": {
"account_number": "4000",
"is_group": 1,
"root_type": "Income",
"Gross Sales": {
"account_number": "4100",
"is_group": 1,
"root_type": "Income",
"Sales": {
"account_number": "4101",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Adjustment": {
"account_number": "4200",
"is_group": 1,
"root_type": "Income",
"Sales Return And Allowance": {
"account_number": "4201",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Discount": {
"account_number": "4300",
"is_group": 1,
"root_type": "Income",
"Sales Discount": {
"account_number": "4301",
"is_group": 0,
"root_type": "Income"
}
},
"Other Income": {
"account_number": "6000",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6010",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6011",
"is_group": 0,
"root_type": "Income"
}
},
"Dividend Income": {
"account_number": "6020",
"is_group": 1,
"root_type": "Income",
"Dividend Income": {
"account_number": "6021",
"is_group": 0,
"root_type": "Income"
}
}
}
},
"Expense": {
"account_number": "5000",
"is_group": 1,
"root_type": "Expense",
"Operating Expenses": {
"account_number": "5100",
"is_group": 1,
"root_type": "Expense",
"Salaries, Wages": {
"account_number": "5101",
"is_group": 0,
"root_type": "Expense"
},
"13th Month Pay & Bonus": {
"account_number": "5102",
"is_group": 0,
"root_type": "Expense"
},
"Overtime & Night Diff": {
"account_number": "5103",
"is_group": 0,
"root_type": "Expense"
},
"Incentive/Performance Bonus": {
"account_number": "5104",
"is_group": 0,
"root_type": "Expense"
},
"Employees Benefits": {
"account_number": "5105",
"is_group": 0,
"root_type": "Expense"
},
"Advertising & Promotions": {
"account_number": "5106",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Leasehold Improvement": {
"account_number": "5107",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Pre-Operating": {
"account_number": "5108",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of System Development": {
"account_number": "5109",
"is_group": 0,
"root_type": "Expense"
},
"Audit & Legal Fee": {
"account_number": "5110",
"is_group": 0,
"root_type": "Expense"
},
"Bad Debts Expenses": {
"account_number": "5111",
"is_group": 0,
"root_type": "Expense"
},
"Client Service & Maintenance": {
"account_number": "5112",
"is_group": 0,
"root_type": "Expense"
},
"Commission Expenses": {
"account_number": "5113",
"is_group": 0,
"root_type": "Expense"
},
"Communications": {
"account_number": "5114",
"is_group": 0,
"root_type": "Expense"
},
"Contractual Services": {
"account_number": "5115",
"is_group": 0,
"root_type": "Expense"
},
"Depreciation Expenses": {
"account_number": "5116",
"is_group": 0,
"root_type": "Expense",
"account_type": "Depreciation"
},
"Donation & Contribution": {
"account_number": "5117",
"is_group": 0,
"root_type": "Expense"
},
"Dues & Subscription": {
"account_number": "5118",
"is_group": 0,
"root_type": "Expense"
},
"Employee Med/Dental/Hosp Expenses": {
"account_number": "5119",
"is_group": 0,
"root_type": "Expense"
},
"Employee Uniforms": {
"account_number": "5120",
"is_group": 0,
"root_type": "Expense"
},
"Equipage": {
"account_number": "5121",
"is_group": 0,
"root_type": "Expense"
},
"Expenses for Reclassification": {
"account_number": "5122",
"is_group": 0,
"root_type": "Expense"
},
"Gas & Oil": {
"account_number": "5123",
"is_group": 0,
"root_type": "Expense"
},
"Insurance Expenses": {
"account_number": "5124",
"is_group": 0,
"root_type": "Expense"
},
"Light & Water": {
"account_number": "5125",
"is_group": 0,
"root_type": "Expense"
},
"Local/Overseas Travel": {
"account_number": "5126",
"is_group": 0,
"root_type": "Expense"
},
"Meals & Transportation Expenses": {
"account_number": "5127",
"is_group": 0,
"root_type": "Expense"
},
"Meeting & Conferences": {
"account_number": "5128",
"is_group": 0,
"root_type": "Expense"
},
"Miscellaneous Expenses": {
"account_number": "5129",
"is_group": 0,
"root_type": "Expense"
},
"Mockup Expenses": {
"account_number": "5130",
"is_group": 0,
"root_type": "Expense"
},
"Obsolescence Expenses": {
"account_number": "5131",
"is_group": 0,
"root_type": "Expense"
},
"Other Support Cost": {
"account_number": "5132",
"is_group": 0,
"root_type": "Expense"
},
"Pag-ibig Contribution": {
"account_number": "5133",
"is_group": 0,
"root_type": "Expense"
},
"Performance Bonds": {
"account_number": "5134",
"is_group": 0,
"root_type": "Expense"
},
"Pre Employment Expenses": {
"account_number": "5135",
"is_group": 0,
"root_type": "Expense"
},
"Professional Fees": {
"account_number": "5136",
"is_group": 0,
"root_type": "Expense"
},
"Recruitment & Employment": {
"account_number": "5137",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses": {
"account_number": "5138",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses Others": {
"account_number": "5139",
"is_group": 0,
"root_type": "Expense"
},
"Repairs & Maintenance": {
"account_number": "5140",
"is_group": 0,
"root_type": "Expense"
},
"Representation Expenses": {
"account_number": "5141",
"is_group": 0,
"root_type": "Expense"
},
"Research & Development": {
"account_number": "5142",
"is_group": 0,
"root_type": "Expense"
},
"Security Expenses": {
"account_number": "5143",
"is_group": 0,
"root_type": "Expense"
},
"Shared Services Fee": {
"account_number": "5144",
"is_group": 0,
"root_type": "Expense"
},
"SSS/Medicare/EC Contributions": {
"account_number": "5145",
"is_group": 0,
"root_type": "Expense"
},
"Stationery & Supplies": {
"account_number": "5146",
"is_group": 0,
"root_type": "Expense"
},
"Taxes & Licenses": {
"account_number": "5147",
"is_group": 0,
"root_type": "Expense",
"account_type": "Tax"
},
"Training & Seminar": {
"account_number": "5148",
"is_group": 0,
"root_type": "Expense"
}
},
"Stock Adjustment": {
"account_number": "5200",
"is_group": 0,
"root_type": "Expense",
"account_type": "Stock Adjustment"
},
"Round Off": {
"account_number": "5300",
"is_group": 0,
"root_type": "Expense",
"account_type": "Round Off"
},
"Expenses Included In Valuation": {
"account_number": "5400",
"is_group": 0,
"root_type": "Expense",
"account_type": "Expenses Included In Valuation"
}
}
}
}

View File

@@ -82,15 +82,13 @@ class AccountingDimension(Document):
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def on_update(self):
def after_insert(self):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
frappe.enqueue(
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
)
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def on_trash(self):
if frappe.flags.in_test:
@@ -105,6 +103,10 @@ class AccountingDimension(Document):
if not self.fieldname:
self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
class OverlapError(frappe.ValidationError):
@@ -35,20 +34,8 @@ class AccountingPeriod(Document):
# end: auto-generated types
def validate(self):
self.validate_dates()
self.validate_overlap()
def validate_dates(self):
if getdate(self.start_date) > getdate(self.end_date):
frappe.throw(_("Start Date cannot be after End Date"))
if getdate(self.end_date) > getdate(nowdate()):
frappe.throw(
_(
"Accounting Period cannot be created for a future date. End Date {0} is after today."
).format(frappe.bold(frappe.format(self.end_date, "Date")))
)
def before_insert(self):
self.bootstrap_doctypes_for_closing()

View File

@@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import nowdate
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
@@ -47,7 +47,7 @@ def create_accounting_period(**args):
accounting_period = frappe.new_doc("Accounting Period")
accounting_period.start_date = args.start_date or nowdate()
accounting_period.end_date = args.end_date or nowdate()
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})

View File

@@ -30,6 +30,16 @@ 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) {

View File

@@ -79,7 +79,6 @@
"acc_frozen_upto",
"ignore_account_closing_balance",
"use_legacy_controller_for_pcv",
"pcv_job_timeout",
"column_break_25",
"frozen_accounts_modifier",
"tab_break_dpet",
@@ -96,6 +95,7 @@
"receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"payment_request_settings",
@@ -216,7 +216,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order/Quotation"
"label": "Automatically Fetch Payment Terms from Order"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -307,7 +307,7 @@
},
{
"default": "0",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"
@@ -561,7 +561,7 @@
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor"
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
@@ -623,6 +623,13 @@
"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",
@@ -652,14 +659,6 @@
"fieldtype": "Check",
"label": "Show Party Balance"
},
{
"default": "3600",
"depends_on": "eval: !doc.use_legacy_controller_for_pcv",
"description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher",
"fieldname": "pcv_job_timeout",
"fieldtype": "Int",
"label": "PCV Job Timeout (seconds)"
},
{
"default": "30, 60, 90, 120",
"fieldname": "default_ageing_range",
@@ -672,7 +671,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-24 12:59:41.868865",
"modified": "2025-12-26 19:46:55.093717",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -60,8 +60,7 @@ class AccountsSettings(Document):
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check
pcv_job_timeout: DF.Int
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
@@ -171,3 +170,11 @@ class AccountsSettings(Document):
),
title=_("Auto Tax Settings Error"),
)
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")

View File

@@ -115,7 +115,7 @@ def get_party_bank_account(party_type, party):
)
def get_default_company_bank_account(company, party_type, party, ignore_permissions=True):
def get_default_company_bank_account(company, party_type, party):
default_company_bank_account = frappe.db.get_value(party_type, party, "default_bank_account")
if default_company_bank_account:
if company != frappe.get_cached_value("Bank Account", default_company_bank_account, "company"):
@@ -126,20 +126,11 @@ def get_default_company_bank_account(company, party_type, party, ignore_permissi
"Bank Account", {"company": company, "is_company_account": 1, "is_default": 1}
)
if not ignore_permissions:
default_company_bank_account = (
default_company_bank_account
if default_company_bank_account
and frappe.get_cached_doc("Bank Account", default_company_bank_account).has_permission("select")
else None
)
return default_company_bank_account
@frappe.whitelist()
def get_bank_account_details(bank_account):
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
return frappe.get_cached_value(
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
)

View File

@@ -5,9 +5,7 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
from pypika import Order
@@ -90,14 +88,7 @@ class BankClearance(Document):
@frappe.whitelist()
def update_clearance_date(self):
payment_docs = []
for d in self.get("payment_entries"):
if d.payment_document not in payment_docs:
payment_docs.append(d.payment_document)
for doctype in payment_docs:
frappe.has_permission(doctype, "write", throw=True)
clearance_date_updated = False
for d in self.get("payment_entries"):
if d.clearance_date:
if not d.payment_document:
@@ -145,162 +136,65 @@ def get_payment_entries_for_bank_clearance(
):
entries = []
journal_entry = frappe.qb.DocType("Journal Entry")
journal_entry_account = frappe.qb.DocType("Journal Entry Account")
journal_entry_query = (
frappe.qb.from_(journal_entry_account)
.inner_join(journal_entry)
.on(journal_entry_account.parent == journal_entry.name)
.select(
ConstantColumn("Journal Entry").as_("payment_document"),
journal_entry.name.as_("payment_entry"),
journal_entry.cheque_no.as_("cheque_number"),
journal_entry.cheque_date,
Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
journal_entry.posting_date,
journal_entry_account.against_account,
journal_entry.clearance_date,
journal_entry_account.account_currency,
)
.where(
(journal_entry_account.account == account)
& (journal_entry.docstatus == 1)
& (journal_entry.posting_date >= from_date)
& (journal_entry.posting_date <= to_date)
& (journal_entry.is_opening == "No")
)
)
condition = ""
pe_condition = ""
if not include_reconciled_entries:
journal_entry_query = journal_entry_query.where(
(journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00")
)
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
journal_entries = (
journal_entry_query.groupby(journal_entry_account.account, journal_entry.name)
.orderby(journal_entry.posting_date)
.orderby(journal_entry.name, order=Order.desc)
).run(as_dict=True)
pe = frappe.qb.DocType("Payment Entry")
company = frappe.qb.DocType("Company")
payment_entry_query = (
frappe.qb.from_(pe)
.join(company)
.on(pe.company == company.name)
.select(
ConstantColumn("Payment Entry").as_("payment_document"),
pe.name.as_("payment_entry"),
pe.reference_no.as_("cheque_number"),
pe.reference_date.as_("cheque_date"),
(
Case()
.when(
pe.paid_from == account,
(
pe.paid_amount
+ (
Case()
.when(
(pe.payment_type == "Pay")
& (company.default_currency == pe.paid_from_account_currency),
pe.base_total_taxes_and_charges,
)
.else_(pe.total_taxes_and_charges)
)
),
)
.else_(0)
).as_("credit"),
(
Case()
.when(pe.paid_from == account, 0)
.else_(
pe.received_amount
+ (
Case()
.when(
company.default_currency == pe.paid_to_account_currency,
pe.base_total_taxes_and_charges,
)
.else_(pe.total_taxes_and_charges)
)
)
).as_("debit"),
pe.posting_date,
Coalesce(pe.party, Case().when(pe.paid_from == account, pe.paid_to).else_(pe.paid_from)).as_(
"against_account"
),
pe.clearance_date,
(
Case()
.when(pe.paid_to == account, pe.paid_to_account_currency)
.else_(pe.paid_from_account_currency)
).as_("account_currency"),
)
.where(
((pe.paid_from == account) | (pe.paid_to == account))
& (pe.docstatus == 1)
& (pe.posting_date >= from_date)
& (pe.posting_date <= to_date)
)
journal_entries = frappe.db.sql(
f"""
select
"Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date,
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
if not include_reconciled_entries:
payment_entry_query = payment_entry_query.where(
(pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00")
)
payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run(
as_dict=True
payment_entries = frappe.db.sql(
f"""
select
"Payment Entry" as payment_document, pe.name as payment_entry,
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
from `tabPayment Entry` as pe
join `tabCompany` c on c.name = pe.company
where
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
{pe_condition}
order by
pe.posting_date ASC, pe.name DESC
""",
{
"account": account,
"from": from_date,
"to": to_date,
},
as_dict=1,
)
acc = frappe.qb.DocType("Account")
pi = frappe.qb.DocType("Purchase Invoice")
paid_purchase_invoices_query = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.bill_no.as_("cheque_number"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
)
.where(
(pi.docstatus == 1)
& (pi.is_paid == 1)
& (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
)
if not include_reconciled_entries:
paid_purchase_invoices_query = paid_purchase_invoices_query.where(
(pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
)
paid_purchase_invoices = (
paid_purchase_invoices_query.orderby(pi.posting_date).orderby(pi.name, order=Order.desc)
).run(as_dict=True)
pos_sales_invoices = []
pos_sales_invoices, pos_purchase_invoices = [], []
if include_pos_transactions:
si_payment = frappe.qb.DocType("Sales Invoice Payment")
si = frappe.qb.DocType("Sales Invoice")
acc = frappe.qb.DocType("Account")
pos_sales_invoices_query = (
pos_sales_invoices = (
frappe.qb.from_(si_payment)
.inner_join(si)
.on(si_payment.parent == si.name)
@@ -323,22 +217,38 @@ def get_payment_entries_for_bank_clearance(
& (si.posting_date >= from_date)
& (si.posting_date <= to_date)
)
)
.orderby(si.posting_date)
.orderby(si.name, order=Order.desc)
).run(as_dict=True)
if not include_reconciled_entries:
pos_sales_invoices_query = pos_sales_invoices_query.where(
(si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00")
pi = frappe.qb.DocType("Purchase Invoice")
pos_purchase_invoices = (
frappe.qb.from_(pi)
.inner_join(acc)
.on(pi.cash_bank_account == acc.name)
.select(
ConstantColumn("Purchase Invoice").as_("payment_document"),
pi.name.as_("payment_entry"),
pi.paid_amount.as_("credit"),
pi.posting_date,
pi.supplier.as_("against_account"),
pi.clearance_date,
acc.account_currency,
ConstantColumn(0).as_("debit"),
)
pos_sales_invoices = (
pos_sales_invoices_query.orderby(si.posting_date).orderby(si.name, order=Order.desc)
.where(
(pi.docstatus == 1)
& (pi.cash_bank_account == account)
& (pi.posting_date >= from_date)
& (pi.posting_date <= to_date)
)
.orderby(pi.posting_date)
.orderby(pi.name, order=Order.desc)
).run(as_dict=True)
entries = (
list(payment_entries)
+ list(journal_entries)
+ list(pos_sales_invoices)
+ list(paid_purchase_invoices)
list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
)
return entries

View File

@@ -9,13 +9,6 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) {
frm.set_query("reference_doctype", function () {
return {
filters: {
name: ["in", ["Sales Order", "Purchase Order"]],
},
};
});
frm.set_query("bank_account", function () {
return {
filters: {

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "ACC-BG-.YYYY.-.#####",
"creation": "2016-12-17 10:43:35.731631",
"doctype": "DocType",
@@ -51,7 +50,8 @@
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
"options": "DocType",
"read_only": 1
},
{
"fieldname": "reference_docname",
@@ -60,14 +60,14 @@
"options": "reference_doctype"
},
{
"depends_on": "eval: doc.reference_doctype == \"Sales Order\"",
"depends_on": "eval: doc.bg_type == \"Receiving\"",
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer"
},
{
"depends_on": "eval: doc.reference_doctype == \"Purchase Order\"",
"depends_on": "eval: doc.bg_type == \"Providing\"",
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
@@ -217,11 +217,11 @@
],
"is_submittable": 1,
"links": [],
"modified": "2026-05-25 18:12:10.768835",
"modified": "2025-09-26 00:38:17.584694",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Guarantee",
"naming_rule": "Expression",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{

View File

@@ -57,7 +57,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
filters.append(["date", "<=", to_date])
if from_date:
filters.append(["date", ">=", from_date])
transactions = frappe.get_list(
transactions = frappe.get_all(
"Bank Transaction",
fields=[
"date",
@@ -82,7 +82,6 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
@frappe.whitelist()
def get_account_balance(bank_account, till_date, company):
# returns account balance till the specified date
frappe.has_permission("Bank Account", "read", bank_account, throw=True)
account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict(
{

View File

@@ -2,15 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("Bank Statement Import", {
onload(frm) {
frm.set_query("bank_account", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
},
setup(frm) {
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
frm.import_in_progress = false;

View File

@@ -1,6 +1,7 @@
{
"actions": [],
"autoname": "format:Bank Statement Import on {creation}",
"beta": 1,
"creation": "2019-08-04 14:16:08.318714",
"doctype": "DocType",
"editable_grid": 1,
@@ -210,11 +211,10 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2026-05-30 20:51:10.353723",
"modified": "2024-06-25 17:32:07.658250",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -230,9 +230,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -115,36 +115,6 @@ class TestBankTransaction(FrappeTestCase):
self.assertEqual(bank_transaction.unallocated_amount, 1700)
self.assertEqual(bank_transaction.payment_entries, [])
# Amending a reconciled payment entry must not carry over its clearance date
def test_clearance_date_cleared_on_amend(self):
bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers)
self.assertTrue(frappe.db.get_value("Payment Entry", payment.name, "clearance_date"))
payment.reload()
payment.cancel()
amended = frappe.copy_doc(payment)
amended.amended_from = payment.name
amended.docstatus = 0
amended.insert()
self.assertFalse(amended.clearance_date)
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc(
@@ -428,7 +398,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc(
{
"doctype": "Customer",
"customer_group": "Individual",
"customer_group": "All Customer Groups",
"customer_type": "Company",
"customer_name": "Poore Simon's",
}
@@ -459,7 +429,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
frappe.get_doc(
{
"doctype": "Customer",
"customer_group": "Individual",
"customer_group": "All Customer Groups",
"customer_type": "Company",
"customer_name": "Fayva",
}

View File

@@ -121,7 +121,7 @@ class BisectAccountingStatements(Document):
cur_node.save()
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def build_tree(self):
frappe.db.delete("Bisect Nodes")

View File

@@ -103,8 +103,8 @@ class Budget(Document):
elif account_details.report_type != "Profit and Loss":
frappe.throw(
_(
"Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense"
).format(self.account)
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
).format(d.account)
)
if d.account in account_list:
@@ -425,11 +425,11 @@ def get_ordered_amount(args):
def get_other_condition(args, for_doc):
condition = f"expense_account = {frappe.db.escape(args.expense_account)}"
condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field")
if budget_against_field and args.get(budget_against_field):
condition += f" and child.{budget_against_field} = {frappe.db.escape(args.get(budget_against_field))}"
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
if args.get("fiscal_year"):
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
@@ -437,7 +437,8 @@ def get_other_condition(args, for_doc):
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
)
condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}"
condition += f""" and parent.{date_field}
between '{start_date}' and '{end_date}' """
return condition

View File

@@ -6,14 +6,12 @@ frappe.provide("erpnext.cheque_print");
frappe.ui.form.on("Cheque Print Template", {
refresh: function (frm) {
if (!frm.doc.__islocal) {
if (frappe.user.has_role("System Manager")) {
frm.add_custom_button(
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
function () {
erpnext.cheque_print.view_cheque_print(frm);
}
).addClass("btn-primary");
}
frm.add_custom_button(
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
function () {
erpnext.cheque_print.view_cheque_print(frm);
}
).addClass("btn-primary");
$(frm.fields_dict.cheque_print_preview.wrapper).empty();

View File

@@ -48,8 +48,6 @@ class ChequePrintTemplate(Document):
@frappe.whitelist()
def create_or_update_cheque_print_format(template_name):
frappe.only_for("System Manager")
if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format")
cheque_print.update(

View File

@@ -11,28 +11,22 @@ frappe.ui.form.on("Currency Exchange Settings", {
},
callback: function (r) {
if (r && r.message) {
let result = [],
params = {};
if (frm.doc.service_provider == "exchangerate.host") {
result = ["result"];
params = {
let result = ["result"];
let params = {
date: "{transaction_date}",
from: "{from_currency}",
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
result = ["rates", "{to_currency}"];
params = {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",
symbols: "{to_currency}",
};
} else if (frm.doc.service_provider == "frankfurter.dev - v2") {
result = ["rate"];
params = {
date: "{transaction_date}",
};
add_param(frm, r.message, params, result);
}
add_param(frm, r.message, params, result);
}
},
});

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-15 11:25:55.873110",
"modified": "2025-11-25 13:03:41.896424",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -121,11 +121,24 @@
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
disabled: DF.Check
req_params: DF.Table[CurrencyExchangeSettingsDetails]
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
@@ -70,14 +70,6 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "base", "value": "{from_currency}"})
self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.dev - v2":
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "rate"})
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
def validate_parameters(self):
params = {}
for row in self.req_params:
@@ -113,20 +105,13 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in [
"exchangerate.host",
"frankfurter.dev",
"frankfurter.app",
"frankfurter.dev - v2",
]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"
elif service_provider == "frankfurter.dev - v2":
api = "api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}"
protocol = "https://"
if use_http:

View File

@@ -2,6 +2,7 @@
"actions": [],
"allow_events_in_timeline": 1,
"autoname": "naming_series:",
"beta": 1,
"creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType",
"engine": "InnoDB",
@@ -399,7 +400,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2026-05-30 20:40:30.851842",
"modified": "2024-11-26 13:46:07.760867",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning",
@@ -448,10 +449,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"title_field": "customer_name",
"track_changes": 1
}
}

View File

@@ -1,6 +1,7 @@
{
"actions": [],
"allow_rename": 1,
"beta": 1,
"creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType",
"editable_grid": 1,
@@ -106,7 +107,7 @@
"link_fieldname": "dunning_type"
}
],
"modified": "2026-05-30 20:40:09.952533",
"modified": "2021-11-13 00:25:35.659283",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning Type",
@@ -150,9 +151,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -22,27 +22,17 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
frappe.call({
method: "check_journal_and_reversal",
method: "check_journal_entry_condition",
doc: frm.doc,
callback: function (r) {
if (r.message) {
if (!r.message.journals_posted) {
frm.add_custom_button(
__("Journal Entries"),
function () {
return frm.events.make_jv(frm);
},
__("Create")
);
} else if (!r.message.reversals_posted) {
frm.add_custom_button(
__("Reversal Journal Entries"),
function () {
return frm.events.make_reverse_journal(frm);
},
__("Create")
);
}
frm.add_custom_button(
__("Journal Entries"),
function () {
return frm.events.make_jv(frm);
},
__("Create")
);
}
},
});
@@ -110,14 +100,6 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
},
});
},
make_reverse_journal: function (frm) {
frappe.call({
method: "make_reverse_journal",
doc: frm.doc,
freeze: true,
freeze_message: __("Reversing Journals..."),
});
},
});
frappe.ui.form.on("Exchange Rate Revaluation Account", {

View File

@@ -8,7 +8,7 @@ from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion, Order
from frappe.query_builder.functions import NullIf, Sum
from frappe.utils import flt, get_link_to_form, nowdate
from frappe.utils import flt, get_link_to_form
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
@@ -90,31 +90,25 @@ class ExchangeRateRevaluation(Document):
)
def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.ignore_linked_doctypes = "GL Entry"
@frappe.whitelist()
def check_journal_and_reversal(self):
def check_journal_entry_condition(self):
exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journals_posted = False
reversals_posted = False
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
qb.from_(jea)
.select(jea.parent)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.isnull()) # omit journals that have reversals
)
.run(pluck="name")
.run()
)
if journals:
gle = qb.DocType("GL Entry")
total_amt = (
@@ -129,31 +123,12 @@ class ExchangeRateRevaluation(Document):
.run()
)
if total_amt and total_amt[0][0] == self.total_gain_loss:
journals_posted = True
if total_amt and total_amt[0][0] != self.total_gain_loss:
return True
else:
journals_posted = False
return False
# reverse journals
reverse_journals = (
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.notnull())
)
.run(pluck="name")
)
if reverse_journals:
reversals_posted = True
else:
reversals_posted = False
return {"journals_posted": journals_posted, "reversals_posted": reversals_posted}
return True
def fetch_and_calculate_accounts_data(self):
accounts = self.get_accounts_data()
@@ -367,7 +342,6 @@ class ExchangeRateRevaluation(Document):
@frappe.whitelist()
def make_jv_entries(self):
frappe.has_permission("Journal Entry", "write", throw=True)
zero_balance_jv = self.make_jv_for_zero_balance()
if zero_balance_jv:
frappe.msgprint(
@@ -597,38 +571,6 @@ class ExchangeRateRevaluation(Document):
journal_entry.save()
return journal_entry
@frappe.whitelist()
def make_reverse_journal(self):
frappe.has_permission("Journal Entry", "write", throw=True)
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.isnull()) # omit journals that have reversals
)
.run(pluck="name")
)
if journals:
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
for x in journals:
reversal = make_reverse_journal_entry(x)
reversal.posting_date = nowdate()
reversal.submit()
frappe.msgprint(
_("Revaluation journal for {0} has been created: {1}").format(
frappe.bold(x), get_link_to_form("Journal Entry", reversal.name)
)
)
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
"""
@@ -677,10 +619,6 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
def get_account_details(
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
):
if not account:
return
frappe.has_permission("Account", doc=account, throw=True)
if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory"))

View File

@@ -130,8 +130,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
err = err.save().submit()
# Create JV for ERR
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
self.assertTrue(err.check_journal_entry_condition())
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
@@ -214,8 +213,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
err = err.save().submit()
# Create JV for ERR
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
self.assertTrue(err.check_journal_entry_condition())
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
@@ -289,83 +287,3 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
for key, _val in expected_data.items():
self.assertEqual(expected_data.get(key), account_details.get(key))
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_05_revaluation_journal_reversal(self):
"""
Test reversing of revaluation journals
"""
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = self.company
err.posting_date = today()
err.fetch_and_calculate_accounts_data()
self.assertEqual(len(err.accounts), 1)
err.save().submit()
gain_loss_account = err.get_for_unrealized_gain_loss_account()
usd_account = err.accounts[0].account
old_balance = err.accounts[0].balance_in_base_currency
new_balance = err.accounts[0].new_balance_in_base_currency
total_gain_loss = err.total_gain_loss
# Create JV for ERR
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
self.assertEqual(len(je.accounts), 3)
expected = [
(usd_account, new_balance, 0.0, 100.0, 0.0),
(usd_account, 0.0, old_balance, 0.0, 100.0),
(gain_loss_account, 0.0, total_gain_loss, 0.0, total_gain_loss),
]
actual = []
for acc in je.accounts:
actual.append(
(
acc.account,
acc.debit,
acc.credit,
acc.debit_in_account_currency,
acc.credit_in_account_currency,
)
)
self.assertEqual(expected, actual)
# Assert reversals are not posted
ret = err.check_journal_and_reversal()
self.assertTrue(ret.get("journals_posted"))
self.assertFalse(ret.get("reversals_posted"))
err.make_reverse_journal()
ret = err.check_journal_and_reversal()
self.assertTrue(ret.get("journals_posted"))
self.assertTrue(ret.get("reversals_posted"))
reverse_jv = frappe.db.get_all(
"Journal Entry", filters={"reversal_of": err_journals.get("revaluation_jv")}, pluck="name"
)
self.assertIsNotNone(reverse_jv)

View File

@@ -1,41 +1,108 @@
{
"actions": [],
"creation": "2018-11-22 23:47:02.804568",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"tax_type",
"tax_rate"
],
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-11-22 23:47:02.804568",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"fieldname": "tax_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax",
"options": "Account",
"reqd": 1
},
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"fieldname": "tax_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_rate",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"istable": 1,
"links": [],
"modified": "2026-04-30 23:49:27.020639",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-12-21 23:51:39.445198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@@ -40,10 +40,6 @@ frappe.ui.form.on("Journal Entry", {
},
refresh: function (frm) {
if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) {
frm.set_read_only();
}
erpnext.toggle_naming_series();
if (frm.doc.docstatus > 0) {
@@ -382,17 +378,15 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
accounts_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
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;
}
});
}
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;
}
});
// set difference
if (doc.difference) {

View File

@@ -21,7 +21,6 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.general_ledger import validate_opening_entry_against_pcv
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
@@ -124,9 +123,6 @@ class JournalEntry(AccountsController):
if not self.is_opening:
self.is_opening = "No"
if self.is_opening == "Yes":
validate_opening_entry_against_pcv(self.company)
self.clearance_date = None
self.validate_party()
@@ -218,8 +214,6 @@ class JournalEntry(AccountsController):
def on_cancel(self):
# References for this Journal are removed on the `on_cancel` event in accounts_controller
super().on_cancel()
from_doc_events = getattr(self, "ignore_linked_doctypes", ())
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
@@ -232,10 +226,6 @@ class JournalEntry(AccountsController):
"Unreconcile Payment Entries",
"Advance Payment Ledger Entry",
)
if from_doc_events and from_doc_events != self.ignore_linked_doctypes:
self.ignore_linked_doctypes = self.ignore_linked_doctypes + from_doc_events
self.make_gl_entries(1)
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
@@ -273,9 +263,6 @@ class JournalEntry(AccountsController):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_stock_accounts(self):
if not erpnext.is_perpetual_inventory_enabled(self.company):
return
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
for account in stock_accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
@@ -1188,11 +1175,7 @@ class JournalEntry(AccountsController):
self.validate_total_debit_and_credit()
def get_values(self):
cond = (
f" and outstanding_amount <= {flt(self.write_off_amount)}"
if flt(self.write_off_amount) > 0
else ""
)
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
if self.write_off_based_on == "Accounts Receivable":
return frappe.db.sql(

View File

@@ -1,15 +1,12 @@
frappe.listview_settings["Journal Entry"] = {
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark", "reversal_of"],
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"],
get_indicator: function (doc) {
if (doc.docstatus == 0) {
return [__("Draft", "red", "docstatus,=,0")];
} else if (doc.docstatus == 2) {
return [__("Cancelled", "grey", "docstatus,=,2")];
} else if (doc.docstatus === 1) {
if (doc.reversal_of && doc.voucher_type == "Exchange Rate Revaluation") {
return [__("Reversal Of Exchange Rate Revaluation"), "blue"];
}
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];
} else {
return [__(doc.voucher_type), "blue", "voucher_type,=," + doc.voucher_type];
}
},
};

View File

@@ -24,22 +24,15 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
setTimeout(
() => {
frm.doc.import_in_progress = false;
frm.clear_table("invoices");
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress();
if (!data.errors) {
frm.clear_table("invoices");
frm.refresh_fields();
const message =
frm.doc.invoice_type == "Sales"
? __("Opening Sales Invoice(s) have been created.")
: __("Opening Purchase Invoice(s) have been created.");
frappe.show_alert({
message: message,
indicator: "green",
});
if (frm.doc.invoice_type == "Sales") {
frappe.msgprint(__("Opening Sales Invoices have been created."));
} else {
frm.refresh_fields();
frappe.msgprint(__("Opening Purchase Invoices have been created."));
}
},
1500,
@@ -82,31 +75,29 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
},
setup_company_filters: function (frm) {
frm.events.apply_company_query_filter(frm, "cost_center", "invoices", { is_group: 0 });
frm.events.apply_company_query_filter(frm, "project", "invoices");
frm.events.apply_company_query_filter(frm, "project");
frm.events.apply_company_query_filter(frm, "cost_center", undefined, { is_group: 0 });
frm.events.apply_company_query_filter(frm, "temporary_opening_account", "invoices", {
account_type: "Temporary",
is_group: 0,
});
},
apply_company_query_filter: function (frm, field_name, child_doctype = null, filters = {}) {
const query = function (doc) {
frm.set_query("cost_center", "invoices", function (doc, cdt, cdn) {
return {
filters: {
company: doc.company,
...filters,
},
};
};
});
if (child_doctype) {
frm.set_query(field_name, child_doctype, query);
} else {
frm.set_query(field_name, query);
}
frm.set_query("cost_center", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
frm.set_query("temporary_opening_account", "invoices", function (doc, cdt, cdn) {
return {
filters: {
company: doc.company,
},
};
});
},
company: function (frm) {
@@ -130,7 +121,10 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
},
invoice_type: function (frm) {
frm.clear_table("invoices");
$.each(frm.doc.invoices, (idx, row) => {
row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier";
row.party = "";
});
frm.refresh_fields();
},
@@ -171,19 +165,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
});
frappe.ui.form.on("Opening Invoice Creation Tool Item", {
invoices_add: (frm, cdt, cdn) => {
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
["project", "cost_center"].forEach((fieldname) => {
if (frm.doc[fieldname]) {
frappe.model.set_value(cdt, cdn, fieldname, frm.doc[fieldname]);
} else {
field_copy.push(fieldname);
}
});
frm.script_manager.copy_from_first_row("invoices", row, field_copy);
invoices_add: (frm) => {
frm.trigger("update_invoice_table");
},
});

View File

@@ -1,6 +1,6 @@
{
"actions": [],
"allow_copy": 1,
"beta": 1,
"creation": "2017-08-29 02:22:54.947711",
"doctype": "DocType",
"editable_grid": 1,
@@ -64,10 +64,10 @@
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"collapsible": 1,
@@ -82,8 +82,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
"links": [],
"modified": "2026-05-30 20:43:36.282738",
"modified": "2022-01-04 15:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",
@@ -100,9 +99,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import escape_html, flt, nowdate
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue, is_job_enqueued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -31,7 +31,6 @@ class OpeningInvoiceCreationTool(Document):
create_missing_party: DF.Check
invoice_type: DF.Literal["Sales", "Purchase"]
invoices: DF.Table[OpeningInvoiceCreationToolItem]
project: DF.Link | None
# end: auto-generated types
def onload(self):
@@ -85,11 +84,6 @@ class OpeningInvoiceCreationTool(Document):
)
prepare_invoice_summary(doctype, invoices)
invoices_summary_companies = list(invoices_summary.keys())
for company in invoices_summary_companies:
invoices_summary[escape_html(company)] = invoices_summary.pop(company)
return invoices_summary, max_count
def validate_company(self):
@@ -122,17 +116,6 @@ class OpeningInvoiceCreationTool(Document):
if not row.get(scrub(d)):
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
self.validate_temporary_opening_account(row)
def validate_temporary_opening_account(self, row):
account_type = frappe.get_cached_value("Account", row.temporary_opening_account, "account_type")
if account_type != "Temporary":
frappe.throw(
_("Row #{0}: {1} account is not of type {2}").format(
row.idx, row.temporary_opening_account, "Temporary"
)
)
def get_invoices(self):
invoices = []
for row in self.invoices:
@@ -202,7 +185,6 @@ class OpeningInvoiceCreationTool(Document):
"description": row.item_name or "Opening Invoice Item",
income_expense_account_field: row.temporary_opening_account,
"cost_center": cost_center,
"project": row.get("project") or self.get("project"),
}
)
@@ -266,32 +248,22 @@ class OpeningInvoiceCreationTool(Document):
def start_import(invoices):
errors = 0
names = []
total = len(invoices)
for idx, d in enumerate(invoices):
# Scope each invoice to a savepoint so a failure only undoes that invoice.
# A plain rollback() would discard the whole transaction — including invoices
# imported earlier in this batch and the error logs of earlier failures (the
# latter only survive on mariadb because the Error Log table is MyISAM; on
# postgres they would be lost). Rolling back to a savepoint keeps both.
savepoint = f"opening_invoice_{frappe.generate_hash(length=8)}"
frappe.db.savepoint(savepoint)
is_last = idx == total - 1
try:
invoice_number = None
if d.invoice_number:
invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
doc.insert(set_name=invoice_number)
doc.submit()
frappe.db.commit()
names.append(doc.name)
publish(idx, total, d.doctype, errors=errors if is_last else None)
except Exception:
errors += 1
frappe.db.rollback()
doc.log_error("Opening invoice creation failed")
publish(idx, total, d.doctype, errors=errors if is_last else None)
if errors:
frappe.msgprint(
_("You had {} errors while creating opening invoices. Check {} for more details").format(
@@ -303,7 +275,7 @@ def start_import(invoices):
return names
def publish(index, total, doctype, errors=None):
def publish(index, total, doctype):
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
@@ -311,7 +283,6 @@ def publish(index, total, doctype, errors=None):
message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
count=index + 1,
total=total,
errors=errors,
),
user=frappe.session.user,
)

View File

@@ -3,7 +3,6 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
@@ -12,7 +11,6 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
from erpnext.projects.doctype.project.test_project import make_project
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
@@ -29,26 +27,21 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
self,
invoice_type="Sales",
company=None,
invoices=None,
project=None,
cost_center=None,
party_1=None,
party_2=None,
invoice_number=None,
department=None,
return_doc=False,
):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(
invoice_type=invoice_type,
company=company,
invoices=invoices,
project=project,
cost_center=cost_center,
party_1=party_1,
party_2=party_2,
invoice_number=invoice_number,
department=department,
)
doc.update(args)
if return_doc:
return doc
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
@@ -57,8 +50,8 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
self.assertEqual(len(invoices), 2)
expected_value = {
"keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 200, "Overdue"],
1: ["_Test Customer 1", 200, "Overdue"],
0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"],
}
self.check_expected_values(invoices, expected_value)
@@ -75,34 +68,48 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
for field_idx, field in enumerate(expected_value["keys"]):
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_invoice_requires_temporary_account_type(self):
doc = self.make_invoices(company="_Test Opening Invoice Company", return_doc=True)
doc.invoices[0].temporary_opening_account = "Sales - _TOIC"
self.assertRaises(frappe.ValidationError, doc.make_invoices)
def test_opening_purchase_invoice_creation(self):
invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
"keys": ["supplier", "outstanding_amount", "status"],
0: ["_Test Supplier", 200, "Overdue"],
1: ["_Test Supplier 1", 200, "Overdue"],
0: ["_Test Supplier", 300, "Overdue"],
1: ["_Test Supplier 1", 250, "Overdue"],
}
self.check_expected_values(invoices, expected_value, "Purchase")
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
old_default_receivable_account = frappe.db.get_value(
"Company", "_Test Opening Invoice Company", "default_receivable_account"
)
frappe.db.set_value("Company", "_Test Opening Invoice Company", "default_receivable_account", "")
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
frappe.db.set_value("Company", company, "default_receivable_account", "")
self.make_invoices(
company="_Test Opening Invoice Company",
invoices=[{"party": party_1}, {"party": party_2}],
)
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "_Test Opening Invoice Company",
"is_group": 1,
"company": "_Test Opening Invoice Company",
}
)
cc.insert(ignore_mandatory=True)
cc2 = frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "Main",
"is_group": 0,
"company": "_Test Opening Invoice Company",
"parent_cost_center": cc.name,
}
)
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# Check if missing debit account error raised
error_log = frappe.db.exists(
@@ -112,113 +119,74 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
self.assertTrue(error_log)
# teardown
frappe.db.set_value(
"Company",
"_Test Opening Invoice Company",
"default_receivable_account",
old_default_receivable_account,
)
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
inv_num = f"TEST-NEW-INV-{frappe.generate_hash(length=8)}"
invoices = self.make_invoices(
company="_Test Opening Invoice Company",
invoices=[
{"party": party_1, "invoice_number": inv_num},
{"party": party_2},
],
self.make_invoices(
company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
)
self.assertEqual(invoices[0], inv_num)
sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
# teardown
for inv in [sales_inv1, sales_inv2]:
doc = frappe.get_doc("Sales Invoice", inv)
doc.cancel()
def test_opening_invoice_with_accounting_dimension(self):
invoices = self.make_invoices(
invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
)
for invoice in invoices:
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
expected_value = {
"keys": ["customer", "outstanding_amount", "status", "department"],
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
}
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
def test_opening_entry_project_linking(self):
doc = self.make_invoices(
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True
)
project_1 = make_project(
{"project_name": "Test Opening Invoice projecty 01", "company": "_Test Opening Invoice Company"}
)
project_2 = make_project(
{"project_name": "Test Opening Invoice projecty 02", "company": "_Test Opening Invoice Company"}
)
doc.invoices[0].project = project_1.name
doc.invoices[1].project = project_2.name
invoices = doc.make_invoices()
sales_invoice_1 = frappe.get_doc("Sales Invoice", invoices[0])
sales_invoice_2 = frappe.get_doc("Sales Invoice", invoices[1])
self.assertEqual(sales_invoice_1.items[0].project, project_1.name)
self.assertEqual(sales_invoice_2.items[0].project, project_2.name)
@classmethod
def tearDownClass(cls):
def tearDown(self):
disable_dimension()
super().tearDownClass()
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
default_invoices = []
default_invoice_rows = [
{
"qty": 1.0,
"outstanding_amount": 200,
"party": f"_Test {party}",
"item_name": "Opening Item",
"due_date": add_days(today(), -10),
"posting_date": add_days(today(), -15),
"temporary_opening_account": get_temporary_opening_account(company),
},
{
"qty": 1.0,
"outstanding_amount": 200,
"party": f"_Test {party} 1",
"item_name": "Opening Item",
"due_date": add_days(today(), -10),
"posting_date": add_days(today(), -15),
"temporary_opening_account": get_temporary_opening_account(company),
},
]
for row in args.get("invoices") or default_invoice_rows:
default_invoices.append(
{
"qty": row.get("qty") or 1.0,
"outstanding_amount": row.get("outstanding_amount") or 200,
"party": row.get("party") or f"_Test {party}",
"item_name": row.get("item_name") or "Opening Item",
"due_date": row.get("due_date") or add_days(today(), -10),
"posting_date": row.get("posting_date") or add_days(today(), -15),
"temporary_opening_account": row.get("temporary_opening_account")
or get_temporary_opening_account(company),
"invoice_number": row.get("invoice_number"),
"project": row.get("project"),
"cost_center": row.get("cost_center"),
}
)
invoice_dict = frappe._dict(
{
"company": company,
"invoice_type": args.get("invoice_type", "Sales"),
"project": args.get("project"),
"cost_center": args.get("cost_center"),
"invoices": default_invoices,
"invoices": [
{
"qty": 1.0,
"outstanding_amount": 300,
"party": args.get("party_1") or f"_Test {party}",
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": args.get("invoice_number"),
},
{
"qty": 2.0,
"outstanding_amount": 250,
"party": args.get("party_2") or f"_Test {party} 1",
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": None,
},
],
}
)
invoice_dict.update(args)
invoice_dict.invoices = default_invoices
return invoice_dict
@@ -241,7 +209,7 @@ def make_customer(customer=None):
{
"doctype": "Customer",
"customer_name": customer_name,
"customer_group": "Individual",
"customer_group": "All Customer Groups",
"customer_type": "Company",
"territory": "All Territories",
}

View File

@@ -19,8 +19,7 @@
"qty",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project"
"dimension_col_break"
],
"fields": [
{
@@ -80,7 +79,6 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Outstanding Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
@@ -113,17 +111,11 @@
"fieldname": "invoice_number",
"fieldtype": "Data",
"label": "Invoice Number"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"istable": 1,
"links": [],
"modified": "2026-07-03 15:17:11.938499",
"modified": "2022-03-21 19:31:45.382656",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",
@@ -134,4 +126,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -25,7 +25,6 @@ class OpeningInvoiceCreationToolItem(Document):
party: DF.DynamicLink
party_type: DF.Link | None
posting_date: DF.Date | None
project: DF.Link | None
qty: DF.Data | None
temporary_opening_account: DF.Link | None
# end: auto-generated types

View File

@@ -725,12 +725,31 @@ 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;
frm.events.set_current_exchange_rate(
frm,
"source_exchange_rate",
frm.doc.paid_from_account_currency,
company_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
);
}
}
},
paid_to_account_currency: function (frm) {
@@ -762,28 +781,49 @@ 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) {
frm.set_paid_amount_based_on_received_amount = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_amount && frm.doc.source_exchange_rate) {
if (frm.doc.paid_amount) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
frm.set_value("base_received_amount", frm.doc.base_paid_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);
frm.set_value("received_amount", frm.doc.paid_amount);
} else {
const target_rate =
flt(frm.doc.target_exchange_rate) ||
(company_currency == frm.doc.paid_to_account_currency ? 1 : 0);
if (target_rate) {
frm.set_value("received_amount", flt(frm.doc.base_received_amount) / target_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.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);
}
// set_unallocated_amount is called by below method,
@@ -792,37 +832,6 @@ 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.received_amount && frm.doc.target_exchange_rate) {
frm.set_value(
"base_received_amount",
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
if (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("paid_amount", frm.doc.received_amount);
} else {
const source_rate =
flt(frm.doc.source_exchange_rate) ||
(company_currency == frm.doc.paid_from_account_currency ? 1 : 0);
if (source_rate) {
frm.set_value("paid_amount", flt(frm.doc.base_paid_amount) / source_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);
},
@@ -831,14 +840,11 @@ 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) {
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
if (company_currency == frm.doc.paid_to_account_currency) {
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("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.set_value("base_received_amount", frm.doc.base_paid_amount);
}
}
frm.trigger("reset_received_amount");
@@ -855,14 +861,15 @@ frappe.ui.form.on("Payment Entry", {
);
if (!frm.doc.paid_amount) {
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
if (company_currency == frm.doc.paid_from_account_currency) {
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("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)
);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
}
}
@@ -1782,35 +1789,6 @@ 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", {

View File

@@ -350,7 +350,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.received_amount;",
"depends_on": "doc.received_amount",
"fieldname": "base_received_amount",
"fieldtype": "Currency",
"label": "Received Amount (Company Currency)",
@@ -800,7 +800,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-05-15 13:31:01.166010",
"modified": "2025-05-15 18:01:04.013025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -1197,9 +1197,9 @@ class PaymentEntry(AccountsController):
continue
if tax.add_deduct_tax == "Add":
included_taxes += flt(tax.base_tax_amount)
included_taxes += tax.base_tax_amount
else:
included_taxes -= flt(tax.base_tax_amount)
included_taxes -= tax.base_tax_amount
return included_taxes
@@ -2279,9 +2279,6 @@ def get_outstanding_reference_documents(args, validate=False):
if args.get("party_type") == "Member":
return
if args.get("party_type") and args.get("party"):
frappe.has_permission(args["party_type"], "read", args["party"], throw=True)
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
args["get_outstanding_invoices"] = True
@@ -2309,20 +2306,22 @@ def get_outstanding_reference_documents(args, validate=False):
# Get positive outstanding sales /purchase invoices
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
condition = " and voucher_type={} and voucher_no={}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
condition += " and cost_center='%s'" % args.get("cost_center")
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = {
@@ -2331,19 +2330,18 @@ def get_outstanding_reference_documents(args, validate=False):
}
for fieldname, date_fields in date_fields_dict.items():
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += f" and {fieldname} between {from_date} and {to_date}"
condition += " and {} between '{}' and '{}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= {from_date}"
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= {to_date}"
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
@@ -2558,12 +2556,17 @@ def get_orders_to_be_billed(
if not voucher_type:
return []
# dynamic dimension filters
# Add cost center condition
doc = frappe.get_doc({"doctype": voucher_type})
condition = ""
active_dimensions = get_dimensions(True)[0]
if doc and hasattr(doc, "cost_center") and doc.cost_center:
condition = " and cost_center='%s'" % cost_center
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"
@@ -2691,9 +2694,6 @@ def get_party_details(company, party_type, party, date, cost_center=None):
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
party_account = get_party_account(party_type, party, company)
account_currency = get_account_currency(party_account)
account_balance = (
@@ -2710,7 +2710,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
)
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party, ignore_permissions=False)
bank_account = get_default_company_bank_account(company, party_type, party)
return {
"party_account": party_account,
@@ -2794,8 +2794,7 @@ def get_reference_details(
):
total_amount = outstanding_amount = exchange_rate = account = None
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name)
ref_doc = frappe.get_doc(reference_doctype, reference_name)
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
# Only applies for Reverse Payment Entries
@@ -3041,7 +3040,7 @@ def get_payment_entry(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
)
pe.set_exchange_rate()
pe.set_exchange_rate(ref_doc=doc)
pe.set_amounts()
# If PE is created from PR directly, then no need to find open PRs for the references
@@ -3606,16 +3605,3 @@ def make_payment_order(source_name, target_doc=None):
@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",
)

View File

@@ -200,30 +200,6 @@ class TestPaymentEntry(FrappeTestCase):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
def test_reference_outstanding_amount_on_advance_pull(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=1, rate=1000)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_amount = pe.received_amount = 500
pe.references[0].allocated_amount = 500
pe.insert()
pe.submit()
so.reload()
self.assertEqual(so.advance_paid, 500)
si = make_sales_invoice(so.name)
si.allocate_advances_automatically = 1
si.save()
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
si.submit()
pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
def test_payment_entry_against_pi(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -537,8 +513,6 @@ class TestPaymentEntry(FrappeTestCase):
si.submit()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700)
pe.source_exchange_rate = 50
pe.set_amounts()
pe.reference_no = si.name
pe.reference_date = nowdate()
@@ -614,8 +588,6 @@ class TestPaymentEntry(FrappeTestCase):
pe = get_payment_entry(
"Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
)
pe.source_exchange_rate = 50
pe.set_amounts()
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -1118,27 +1090,6 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(gl_entries, expected_gl_entries)
def test_payment_entry_with_inclusive_tax(self):
# inclusive tax built server-side: base_tax_amount is None until apply_taxes()
payment_entry = create_payment_entry(paid_amount=1180)
payment_entry.append(
"taxes",
{
"account_head": "_Test Account Service Tax - _TC",
"charge_type": "On Paid Amount",
"rate": 18,
"included_in_paid_amount": 1,
"add_deduct_tax": "Add",
"description": "Service Tax",
},
)
payment_entry.save()
payment_entry.submit()
# 1180 incl 18% => 1000 base + 180 tax
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), 180.0)
self.assertEqual(flt(payment_entry.unallocated_amount, 2), 1000.0)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()
@@ -1986,37 +1937,6 @@ class TestPaymentEntry(FrappeTestCase):
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
def test_project_name_in_exchange_gain_loss_entry(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
from erpnext.projects.doctype.project.test_project import make_project
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
si.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.source_exchange_rate = 100
pe.insert()
pe.submit()
rows = frappe.get_all(
"Journal Entry Account",
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
fields=["project"],
)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].project, si.project)
self.assertEqual(rows[1].project, si.project)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
@@ -2123,7 +2043,6 @@ def create_customer(name="_Test Customer 2 USD", currency="USD"):
customer.customer_name = name
customer.default_currency = currency
customer.type = "Individual"
customer.customer_group = "Individual"
customer.save()
customer = customer.name
return customer

View File

@@ -22,7 +22,6 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"in_list_view": 1,
@@ -60,7 +59,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-11 14:26:11.312950",
"modified": "2024-11-05 16:07:47.307971",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",

View File

@@ -3,9 +3,8 @@
import frappe
from frappe import qb
from frappe.query_builder.functions import Count, Sum
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, nowdate
from frappe.utils import 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
@@ -81,7 +80,6 @@ class TestPaymentLedgerEntry(FrappeTestCase):
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.customer_group = "Individual"
customer.save()
self.customer = customer.name
@@ -95,7 +93,6 @@ class TestPaymentLedgerEntry(FrappeTestCase):
posting_date = nowdate()
sinv = create_sales_invoice(
posting_date=posting_date,
qty=qty,
rate=rate,
company=self.company,
@@ -537,82 +534,3 @@ class TestPaymentLedgerEntry(FrappeTestCase):
# with references removed, deletion should be possible
so.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
@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},
)
)

View File

@@ -796,17 +796,10 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
)
for inv in dr_cr_notes:
if (
flt(
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
- inv.allocated_amount,
allocated_amount_precision,
)
< 0
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
< inv.allocated_amount
):
frappe.throw(
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)

View File

@@ -155,7 +155,6 @@ class TestPaymentReconciliation(FrappeTestCase):
sinv = create_sales_invoice(
qty=qty,
rate=rate,
posting_date=posting_date,
company=self.company,
customer=self.customer,
item_code=self.item,
@@ -2147,7 +2146,7 @@ class TestPaymentReconciliation(FrappeTestCase):
pr.reconcile()
si.reload()
self.assertEqual(si.status, "Overdue")
self.assertEqual(si.status, "Partly Paid")
# check PR tool output post reconciliation
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 120)
@@ -2541,83 +2540,12 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0)
pr.reconcile()
def test_cr_note_split_across_invoices_floating_point_precision(self):
"""Regression: when a credit note is split across multiple invoices, floating-point
arithmetic (150 - 8.45 - 90.72 = 50.83000000000001) must not cause reconcile() to fail.
The test environment rounds INR totals to whole rupees (smallest_currency_fraction_value=0),
so the invoices are created with round-number totals (100, 200, 100) and then partially paid
down to the decimal outstanding amounts (8.45, 90.72, 72.57) via payment entries.
"""
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
# Create invoices on different posting dates to control sort-order in Payment Reconciliation
# (invoices are sorted by posting_date ascending, so si_a is processed first).
# Processing order 8.45 → 90.72 → 72.57 produces the float chain:
# 150 - 8.45 = 141.55 → 141.55 - 90.72 = 50.83000000000001
# The last allocation row will therefore carry allocated_amount = 50.83000000000001.
si_a = self.create_sales_invoice(qty=1, rate=100, posting_date=add_days(nowdate(), -2))
si_b = self.create_sales_invoice(qty=1, rate=200, posting_date=add_days(nowdate(), -1))
si_c = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate())
# Partially pay each invoice so the remaining outstanding is a clean decimal value.
# INR rounds the invoice total to a whole rupee, so we achieve decimal outstandings
# by subtracting a decimal-valued payment from the integer total:
# 100 - 91.55 = 8.45
# 200 - 109.28 = 90.72
# 100 - 27.43 = 72.57
for si, partial_paid in ((si_a, 91.55), (si_b, 109.28), (si_c, 27.43)):
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = partial_paid
pe.received_amount = partial_paid
pe.references[0].allocated_amount = partial_paid
pe.save().submit()
cr_note = self.create_sales_invoice(
qty=-1, rate=150, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
# Widen date range so all three invoices (oldest is -2 days) are fetched
pr.from_invoice_date = add_days(nowdate(), -2)
pr.to_invoice_date = nowdate()
pr.from_payment_date = nowdate()
pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 3)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Credit note (150) covers all of si_a (8.45) and si_b (90.72), then partially si_c
self.assertEqual(len(pr.allocation), 3)
last_row = pr.allocation[-1]
# Last allocated amount should be ~50.83 (possibly 50.83000000000001 due to float arithmetic)
self.assertAlmostEqual(flt(last_row.allocated_amount), 50.83, places=2)
# reconcile() must not raise "has been modified after you pulled it" due to float imprecision
pr.reconcile()
si_a.reload()
si_b.reload()
si_c.reload()
self.assertEqual(si_a.outstanding_amount, 0)
self.assertEqual(si_b.outstanding_amount, 0)
# si_c is only partially settled: 72.57 - 50.83 = 21.74
self.assertAlmostEqual(si_c.outstanding_amount, 21.74, places=2)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
customer.customer_group = "Individual"
if currency:
customer.default_currency = currency

View File

@@ -11,12 +11,11 @@ from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_payment_entry,
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account
from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency, get_currency_precision
from erpnext.utilities import payment_app_import_guard
@@ -367,7 +366,6 @@ class PaymentRequest(Document):
bank_amount=bank_amount,
created_from_payment_request=True,
)
payment_entry.set_missing_ref_details(force=True)
payment_entry.update(
{
@@ -835,7 +833,6 @@ def resend_payment_email(docname):
@frappe.whitelist()
def make_payment_entry(docname):
doc = frappe.get_doc("Payment Request", docname)
doc.check_permission("read")
return doc.create_payment_entry(submit=False).as_dict()

View File

@@ -195,12 +195,7 @@ class TestPaymentRequest(FrappeTestCase):
return_doc=1,
)
pe = pr.create_payment_entry(submit=False)
pe.source_exchange_rate = 50
pe.target_exchange_rate = 50
pe.set_amounts()
pe.insert(ignore_permissions=True)
pe.submit()
pe = pr.set_as_paid()
expected_gle = dict(
(d[0], d)
@@ -286,12 +281,7 @@ class TestPaymentRequest(FrappeTestCase):
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
pr = frappe.get_doc(pr).save().submit()
pe = pr.create_payment_entry(submit=False)
pe.target_exchange_rate = 80
pe.paid_amount = 800
pe.set_amounts()
pe.insert(ignore_permissions=True)
pe.submit()
pe = pr.create_payment_entry()
self.assertEqual(pe.base_paid_amount, 800)
self.assertEqual(pe.paid_amount, 800)
self.assertEqual(pe.base_received_amount, 800)
@@ -618,22 +608,6 @@ class TestPaymentRequest(FrappeTestCase):
pi.load_from_db()
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
def test_payment_entry_reference_details_fetched_from_invoice(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=94500)
pi.submit()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1)
pr.grand_total = 94000
pr.submit()
pe = pr.create_payment_entry(submit=False)
self.assertEqual(pe.references[0].reference_name, pi.name)
self.assertEqual(pe.references[0].total_amount, pi.grand_total)
self.assertEqual(pe.references[0].outstanding_amount, pi.outstanding_amount)
self.assertEqual(pe.references[0].allocated_amount, 94000)
self.assertEqual(pe.paid_amount, 94000)
def test_consider_journal_entry_and_return_invoice(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry

View File

@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,

View File

@@ -5,13 +5,11 @@
import unittest
import frappe
from frappe.tests.utils import change_settings
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
@@ -353,54 +351,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
return pcv
@change_settings(
"Accounts Settings",
{"enable_immutable_ledger": 1},
)
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
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")
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", "2021-12-31")
try:
make_reverse_gl_entries(
voucher_type="Journal Entry",
voucher_no=jv.name,
)
finally:
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
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(

View File

@@ -812,7 +812,6 @@
},
{
"default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -859,7 +858,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-04-20 16:16:12.322024",
"modified": "2024-05-07 15:56:54.343317",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -202,14 +202,15 @@ class POSProfile(Document):
def set_defaults(self, include_current_pos=True):
frappe.defaults.clear_default("is_pos")
pfu = frappe.qb.DocType("POS Profile User")
query = frappe.qb.from_(pfu).select(pfu.user).where(pfu.default == 1)
if not include_current_pos:
query = query.where(pfu.name != self.name)
condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'")
else:
condition = " where pfu.default = 1 "
pos_view_users = query.run(as_list=1, pluck=True)
pos_view_users = frappe.db.sql_list(
f"""select pfu.user
from `tabPOS Profile User` as pfu {condition}"""
)
for user in pos_view_users:
if user:
@@ -308,3 +309,32 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
)
return pos_profile
@frappe.whitelist()
def set_default_profile(pos_profile, company):
modified = now()
user = frappe.session.user
if pos_profile and company:
frappe.db.sql(
""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
set
pfu.default = 0, pf.modified = %s, pf.modified_by = %s
where
pfu.user = %s and pf.name = pfu.parent and pf.company = %s
and pfu.default = 1""",
(modified, user, user, company),
auto_commit=1,
)
frappe.db.sql(
""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
set
pfu.default = 1, pf.modified = %s, pf.modified_by = %s
where
pfu.user = %s and pf.name = pfu.parent and pf.company = %s and pf.name = %s
""",
(modified, user, user, company, pos_profile),
auto_commit=1,
)

View File

@@ -156,24 +156,6 @@ class PricingRule(Document):
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
if self.apply_on == "Item Code":
self.validate_template_with_variant(values)
def validate_template_with_variant(self, item_codes):
# throws if a template and its variant both exist in one rule
variants = frappe.get_all(
"Item",
filters={"name": ("in", item_codes), "variant_of": ("in", item_codes)},
fields=["name", "variant_of"],
)
if variants:
variant = variants[0]
frappe.throw(
_("Variant {0} and its template {1} cannot both be added to the same Pricing Rule").format(
frappe.bold(variant.name), frappe.bold(variant.variant_of)
)
)
def validate_mandatory(self):
if self.has_priority and not self.priority:
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))

View File

@@ -336,31 +336,6 @@ class TestPricingRule(FrappeTestCase):
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 17.5)
def test_pricing_rule_with_template_and_its_variant(self):
if not frappe.db.exists("Item", "Test Variant PRT"):
variant = frappe.new_doc("Item")
variant.item_code = "Test Variant PRT"
variant.item_name = "Test Variant PRT"
variant.item_group = "_Test Item Group"
variant.is_stock_item = 1
variant.variant_of = "_Test Variant Item"
variant.stock_uom = "_Test UOM"
variant.append("attributes", {"attribute": "Test Size", "attribute_value": "Medium"})
variant.insert()
rule = frappe.new_doc("Pricing Rule")
rule.title = "_Test Pricing Rule Template Variant"
rule.apply_on = "Item Code"
rule.currency = "USD"
rule.selling = 1
rule.rate_or_discount = "Discount Percentage"
rule.discount_percentage = 10
rule.company = "_Test Company"
rule.append("items", {"item_code": "_Test Variant Item"})
rule.append("items", {"item_code": "Test Variant PRT"})
self.assertRaises(frappe.ValidationError, rule.insert)
def test_pricing_rule_for_stock_qty(self):
test_record = {
"doctype": "Pricing Rule",

View File

@@ -243,10 +243,8 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
date = (
args.get("transaction_date")
or args.get("posting_date")
or frappe.get_value(args.get("doctype"), args.get("name"), "posting_date", ignore=True)
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
@@ -658,7 +656,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if pricing_rule.is_recursive:
transaction_qty = sum(
[
flt(row.qty)
row.qty
for row in doc.items
if not row.is_free_item
and row.item_code == args.item_code

View File

@@ -151,13 +151,13 @@
"label": "Default Advance Account",
"mandatory_depends_on": "doc.party_type",
"options": "Account",
"reqd": 0
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-05-16 11:43:12.758685",
"modified": "2025-01-08 08:22:14.798085",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",

View File

@@ -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 | None
default_advance_account: DF.Link
error_log: DF.LongText | None
from_invoice_date: DF.Date | None
from_payment_date: DF.Date | None
@@ -128,7 +128,6 @@ def is_job_running(job_name: str) -> bool:
@frappe.whitelist()
def pause_job_for_doc(docname: str | None = None):
if docname:
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
@@ -143,8 +142,6 @@ def trigger_job_for_doc(docname: str | None = None):
if not docname:
return
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
@@ -218,7 +215,10 @@ def trigger_reconciliation_for_queued_docs():
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
def get_filters_as_tuple(fields, doc):
return tuple(doc.get(x) or "" for x in fields)
filters = ()
for x in fields:
filters += tuple(doc.get(x))
return filters
for x in all_queued:
doc = frappe.get_doc("Process Payment Reconciliation", x)

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "format:Process-PCV-{###}",
"creation": "2025-09-25 15:44:03.534699",
"doctype": "DocType",
@@ -8,13 +7,11 @@
"field_order": [
"parent_pcv",
"status",
"amended_from",
"section_normal_balances",
"p_l_closing_balance",
"bs_closing_balance",
"normal_balances",
"section_opening_balances",
"z_opening_balances"
"bs_closing_balance",
"z_opening_balances",
"amended_from"
],
"fields": [
{
@@ -67,27 +64,17 @@
"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": "2026-06-01 12:16:37.374412",
"modified": "2025-11-05 11:40:24.996403",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Period Closing Voucher",
"naming_rule": "Expression (old style)",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@@ -69,8 +69,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)).run()[0][0]
max = qb.from_(gl).select(Max(gl.posting_date)).run()[0][0]
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]
dates = self.get_dates(get_datetime(min), get_datetime(max))
for x in dates:
@@ -86,55 +86,43 @@ class ProcessPeriodClosingVoucher(Document):
cancel_pcv_processing(self.name)
def initialize_parallel_threads(docname: str):
threads = 4
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
if normal_balances := (
qb.from_(ppcvd)
.select(ppcvd.name, 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(threads)
.for_update(skip_locked=True)
.run(as_dict=True)
):
if not is_scheduler_inactive():
for x in normal_balances:
frappe.db.set_value(
"Process Period Closing Voucher Detail",
x.name,
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout=timeout,
is_async=True,
enqueue_after_commit=True,
docname=docname,
row_name=x.name,
date=x.processing_date,
report_type=x.report_type,
parentfield=x.parentfield,
)
# keep transaction on PPCV and PPCVD short
# prevents concurrency errors - REPEATABLE READ
if not frappe.in_test:
frappe.db.commit() # nosemgrep
else:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@frappe.whitelist()
def start_pcv_processing(docname: str):
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True)
initialize_parallel_threads(docname)
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,
):
if not is_scheduler_inactive():
for x in normal_balances:
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{
"processing_date": x.processing_date,
"parent": docname,
"report_type": x.report_type,
"parentfield": x.parentfield,
},
"status",
"Running",
)
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout="3600",
is_async=True,
enqueue_after_commit=True,
docname=docname,
date=x.processing_date,
report_type=x.report_type,
parentfield=x.parentfield,
)
else:
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@frappe.whitelist()
@@ -142,10 +130,9 @@ 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": ["in", ["Queued", "Running"]]},
filters={"parent": docname, "status": "Queued"},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
@@ -179,9 +166,6 @@ 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):
@@ -251,38 +235,32 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
@frappe.whitelist()
def schedule_next_date(docname: str):
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
if to_process := (
qb.from_(ppcvd)
.select(ppcvd.name, 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 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,
):
if not is_scheduler_inactive():
frappe.db.set_value(
"Process Period Closing Voucher Detail",
to_process[0].name,
{
"processing_date": to_process[0].processing_date,
"parent": docname,
"report_type": to_process[0].report_type,
"parentfield": to_process[0].parentfield,
},
"status",
"Running",
)
# keep transaction on PPCV and PPCVD short
# prevents concurrency errors - REPEATABLE READ
if not frappe.in_test:
frappe.db.commit() # nosemgrep
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
queue="long",
timeout=timeout,
timeout="3600",
is_async=True,
enqueue_after_commit=True,
docname=docname,
row_name=to_process[0].name,
date=to_process[0].processing_date,
report_type=to_process[0].report_type,
parentfield=to_process[0].parentfield,
@@ -300,21 +278,7 @@ def schedule_next_date(docname: str):
)
# Ensure both normal and opening balances are processed for all dates
if total_no_of_dates == completed:
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=timeout,
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
docname=docname,
)
summarize_and_post_ledger_entries(docname)
def make_dict_json_compliant(dimension_wise_balance) -> dict:
@@ -447,11 +411,6 @@ def summarize_and_post_ledger_entries(docname):
make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date)
# keep transaction on PPCV and PPCVD short
# prevents concurrency errors - REPEATABLE READ
if not frappe.in_test:
frappe.db.commit() # nosemgrep
frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed")
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@@ -537,10 +496,10 @@ def build_dimension_wise_balance_dict(gl_entries):
return dimension_balances
def process_individual_date(docname: str, row_name, date, report_type, parentfield):
def process_individual_date(docname: str, date, report_type, parentfield):
current_date_status = frappe.db.get_value(
"Process Period Closing Voucher Detail",
row_name,
{"processing_date": date, "report_type": report_type, "parentfield": parentfield},
"status",
)
if current_date_status != "Running":
@@ -575,9 +534,6 @@ def process_individual_date(docname: str, row_name, date, report_type, parentfie
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:
@@ -587,20 +543,17 @@ def process_individual_date(docname: str, row_name, date, report_type, parentfie
# save results
frappe.db.set_value(
"Process Period Closing Voucher Detail",
row_name,
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
"closing_balance",
frappe.json.dumps(res),
)
frappe.db.set_value(
"Process Period Closing Voucher Detail",
row_name,
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
"status",
"Completed",
)
# commit heavy computation before touching PPCV or PPCVD
if not frappe.in_test:
frappe.db.commit() # nosemgrep
# chain call
schedule_next_date(docname)

View File

@@ -1,17 +0,0 @@
// 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];
},
};

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
# import frappe
from frappe.model.document import Document
@@ -24,10 +24,3 @@ class ProcessPeriodClosingVoucherDetail(Document):
# end: auto-generated types
pass
def on_doctype_update():
frappe.db.add_index(
"Process Period Closing Voucher Detail",
["parent", "status", "parentfield", "idx", "processing_date"],
)

View File

@@ -13,7 +13,7 @@
</div>
{% endif %}
</div>
<h2 class="text-center">{{ _("STATEMENT OF ACCOUNTS") }}</h2>
<h2 class="text-center">{{ _("GENERAL LEDGER") }}</h2>
<div>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>

View File

@@ -99,7 +99,6 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.subject)
validate_template(self.body)
validate_template(self.pdf_name)
if not self.customers:
frappe.throw(_("Customers not selected."))
@@ -390,6 +389,7 @@ def get_context(customer, doc):
return {
"doc": template_doc,
"customer": frappe.get_doc("Customer", customer),
"frappe": frappe.utils,
}
@@ -439,8 +439,6 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
when Is Billing Contact checked
and Primary email- email with Is Primary checked"""
frappe.has_permission("Customer", "read", customer_name, throw=True)
billing_email = frappe.db.sql(
"""
SELECT
@@ -484,7 +482,6 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
@frappe.whitelist()
def download_statements(document_name):
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
doc.check_permission("read")
report = get_report_pdf(doc)
if report:
frappe.local.response.filename = doc.name + ".pdf"
@@ -495,7 +492,6 @@ def download_statements(document_name):
@frappe.whitelist()
def send_emails(document_name, from_scheduler=False, posting_date=None):
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
doc.check_permission()
report = get_report_pdf(doc, consolidated=False)
if report:
@@ -552,7 +548,6 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
@frappe.whitelist()
def send_auto_email():
frappe.has_permission("Process Statement Of Accounts", throw=True)
selected = frappe.get_list(
"Process Statement Of Accounts",
filters={"enable_auto_email": 1},

View File

@@ -21,12 +21,10 @@ frappe.ui.form.on("Promotional Scheme", {
selling: function (frm) {
frm.trigger("set_options_for_applicable_for");
frm.toggle_enable("buying", !frm.doc.selling);
},
buying: function (frm) {
frm.trigger("set_options_for_applicable_for");
frm.toggle_enable("selling", !frm.doc.buying);
},
set_options_for_applicable_for: function (frm) {

View File

@@ -115,12 +115,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (
doc.docstatus == 1 &&
doc.outstanding_amount != 0 &&
!doc.on_hold &&
frappe.model.can_create("Payment Entry")
) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -131,13 +126,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (
doc.docstatus == 1 &&
doc.outstanding_amount > 0 &&
!cint(doc.is_return) &&
!doc.on_hold &&
frappe.boot.user.in_create.includes("Payment Request")
) {
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -471,14 +460,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"discount_account",
"cost_center",
"project",
]);
}
on_submit() {
@@ -587,6 +575,12 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
frappe.ui.form.on("Purchase Invoice", {
setup: function (frm) {
frm.custom_make_buttons = {
@@ -608,25 +602,6 @@ 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: {

View File

@@ -312,7 +312,7 @@
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"label": "Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"print_hide": 1,
@@ -382,7 +382,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "posting_date",
"collapsible_depends_on": "bill_no",
"fieldname": "supplier_invoice_details",
"fieldtype": "Section Break",
"label": "Supplier Invoice"
@@ -1397,10 +1397,8 @@
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
"options": "Company"
},
{
"depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
@@ -1662,7 +1660,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-07-12 23:54:21.263951",
"modified": "2026-02-05 20:45:16.964500",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -287,7 +287,6 @@ 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()
@@ -613,13 +612,12 @@ class PurchaseInvoice(BuyingController):
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
def po_required(self):
if (
frappe.db.get_single_value("Buying Settings", "po_required") == "Yes"
and not self.is_internal_transfer()
and not frappe.db.get_value(
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
)
):
):
return
for d in self.get("items"):
if not d.purchase_order:
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
@@ -634,16 +632,15 @@ class PurchaseInvoice(BuyingController):
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
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())
stock_items = self.get_stock_items()
if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes":
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_and_asset_items:
if not d.purchase_receipt and d.item_code in stock_items:
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _(
@@ -659,27 +656,6 @@ 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:
@@ -752,15 +728,13 @@ class PurchaseInvoice(BuyingController):
for item in self.get("items"):
if item.purchase_receipt:
frappe.throw(
_(
"Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice."
).format(self.name, item.purchase_receipt),
title=_("Stock Update Not Allowed"),
_("Stock cannot be updated against Purchase Receipt {0}").format(
item.purchase_receipt
)
)
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])
@@ -874,9 +848,7 @@ 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,
@@ -1004,10 +976,6 @@ class PurchaseInvoice(BuyingController):
if provisional_accounting_for_non_stock_items:
self.get_provisional_accounts()
adjust_incoming_rate = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
for item in self.get("items"):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if item.item_code:
@@ -1176,11 +1144,7 @@ class PurchaseInvoice(BuyingController):
)
# check if the exchange rate has changed
if (
not adjust_incoming_rate
and item.get("purchase_receipt")
and self.auto_accounting_for_stock
):
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
if (
exchange_rate_map[item.purchase_receipt]
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
@@ -1217,7 +1181,6 @@ class PurchaseInvoice(BuyingController):
item=item,
)
)
if (
self.auto_accounting_for_stock
and self.is_opening == "No"
@@ -1559,9 +1522,6 @@ 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(
@@ -1576,7 +1536,9 @@ class PurchaseInvoice(BuyingController):
if self.party_account_currency == self.company_currency
else self.paid_amount,
"debit_in_transaction_currency": self.paid_amount,
"against_voucher": against_voucher,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
@@ -2086,7 +2048,6 @@ def make_stock_entry(source_name, target_doc=None):
def change_release_date(name, release_date=None):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_doc("Purchase Invoice", name)
pi.check_permission()
pi.db_set("release_date", release_date)

View File

@@ -356,12 +356,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
make_purchase_invoice as create_purchase_invoice,
)
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
warehouse="Stores - TCP1",
@@ -382,17 +376,12 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
)
discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[0].base_net_amount - pr.items[0].base_net_amount
)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
)
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice,
@@ -2924,24 +2913,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
self.assertRaises(frappe.ValidationError, pi.submit)
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_po_is_blocked(self):
service_item = create_item(
"_Test Service Item Non Stock PI",
is_stock_item=0,
is_purchase_item=1,
).name
po = create_purchase_order(item_code=service_item, qty=5, rate=100, do_not_save=False)
po.submit()
pi = make_pi_from_po(po.name)
pi.items[0].qty = 10 # overbill by 100 %
pi.save()
with self.assertRaises(frappe.ValidationError):
pi.submit()
def test_discount_percentage_not_set_when_amount_is_manually_set(self):
pi = make_purchase_invoice(do_not_save=True)
discount_amount = 7

View File

@@ -109,7 +109,6 @@
"sales_invoice_item",
"material_request",
"material_request_item",
"delivered_by_supplier",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -732,6 +731,7 @@
"label": "Valuation Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -979,21 +979,12 @@
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"default": "0",
"fieldname": "delivered_by_supplier",
"fieldtype": "Check",
"hidden": 1,
"label": "Delivered by Supplier",
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-05-06 08:08:40.782395",
"modified": "2025-10-14 13:01:54.441511",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -31,7 +31,6 @@ class PurchaseInvoiceItem(Document):
conversion_factor: DF.Float
cost_center: DF.Link | None
deferred_expense_account: DF.Link | None
delivered_by_supplier: DF.Check
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent

View File

@@ -154,13 +154,12 @@ class RepostAccountingLedger(Document):
@frappe.whitelist()
def start_repost(account_repost_doc: str | None = None) -> None:
def start_repost(account_repost_doc=str) -> None:
from erpnext.accounts.general_ledger import make_reverse_gl_entries
frappe.flags.through_repost_accounting_ledger = True
if account_repost_doc:
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
repost_doc.check_permission("write")
if repost_doc.docstatus == 1:
# Prevent repost on invoices with deferred accounting

View File

@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -136,15 +136,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (doc.outstanding_amount > 0) {
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
},
__("Create")
);
}
cur_frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
},
__("Create")
);
this.frm.add_custom_button(
__("Invoice Discounting"),
this.make_invoice_discounting.bind(this),
@@ -168,7 +166,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
);
}
}
this.toggle_get_items();
// Show buttons only when pos view is active
if (cint(doc.docstatus == 0) && cur_frm.page.current_view_name !== "pos" && !doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.delivery_note_btn();
this.frm.cscript.quotation_btn();
}
this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
@@ -180,31 +184,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
: "Inter Company Purchase Invoice";
me.frm.add_custom_button(
__(button_label),
button_label,
function () {
me.make_inter_company_invoice();
},
__("Create")
);
frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_received_items",
args: {
reference_name: me.frm.doc.name,
doctype: "Purchase Invoice",
reference_fieldname: "sales_invoice_item",
},
callback: function (r) {
if (r.exc) return;
const received_items = r.message || {};
const has_pending_qty = me.frm.doc.items.some(
(item) => flt(item.qty) - flt(received_items[item.name] || 0) > 0
);
if (!has_pending_qty) {
me.frm.remove_custom_button(__(button_label), __("Create"));
}
},
});
}
}
@@ -273,93 +258,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
}
toggle_get_items() {
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
buttons.forEach((label) => {
this.frm.remove_custom_button(label, "Get Items From");
});
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
return;
}
if (!this.frm.doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.quotation_btn();
this.frm.cscript.timesheet_btn();
}
this.frm.cscript.delivery_note_btn();
}
timesheet_btn() {
var me = this;
me.frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: me.frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: me.frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
me.frm.events.add_timesheet_data(me.frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
sales_order_btn() {
var me = this;
this.$sales_order_btn = this.frm.add_custom_button(
@@ -424,12 +322,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.$delivery_note_btn = this.frm.add_custom_button(
__("Delivery Note"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_doctype: "Delivery Note",
@@ -442,7 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
var filters = {
docstatus: 1,
company: me.frm.doc.company,
is_return: me.frm.doc.is_return,
is_return: 0,
};
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
return {
@@ -561,14 +453,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["income_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"income_account",
"discount_account",
"cost_center",
]);
}
set_dynamic_labels() {
@@ -704,14 +594,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.calculate_taxes_and_totals();
}
apply_tds(frm) {
this.frm.clear_table("tax_withholding_entries");
}
is_return() {
this.toggle_get_items();
}
};
// for backward compatibility: combine new and previous states
@@ -1157,6 +1039,71 @@ frappe.ui.form.on("Sales Invoice", {
},
refresh: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
if (frm.doc.is_debit_note) {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}

View File

@@ -373,7 +373,7 @@
"fieldtype": "Date",
"hide_days": 1,
"hide_seconds": 1,
"label": "Posting Date",
"label": "Date",
"no_copy": 1,
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
@@ -777,7 +777,8 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
"depends_on": "eval:!doc.is_return",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_border": 1,
@@ -791,6 +792,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Time Sheets",
"no_copy": 1,
"options": "Sales Invoice Timesheet",
"print_hide": 1
},
@@ -2110,7 +2112,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
"fieldname": "section_break_104",
"fieldtype": "Section Break"
},
@@ -2198,7 +2200,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-04-06 22:30:28.513139",
"modified": "2026-02-05 20:43:44.732805",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -23,12 +23,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
get_party_tax_withholding_details,
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import (
CROSS_PARTY_FIELD_NO_MAP,
get_due_date,
get_party_account,
get_party_details,
)
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
@@ -328,22 +323,10 @@ class SalesInvoice(SellingController):
)
self.set_against_income_account()
if self.is_return and not self.return_against and self.timesheets:
frappe.throw(_("Direct return is not allowed for Timesheet."))
if not self.is_return:
self.validate_time_sheets_are_submitted()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if self.is_return and self.return_against:
for row in self.timesheets:
if row.billing_hours:
row.billing_hours = -abs(row.billing_hours)
if row.billing_amount:
row.billing_amount = -abs(row.billing_amount)
if self.is_return:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -455,8 +438,8 @@ class SalesInvoice(SellingController):
self.calculate_taxes_and_totals()
def before_save(self):
self.set_paid_amount()
self.set_account_for_mode_of_payment()
self.set_paid_amount()
def before_submit(self):
self.add_remarks()
@@ -511,7 +494,7 @@ class SalesInvoice(SellingController):
if not cint(self.is_pos) == 1 and not self.is_return:
self.update_against_document_in_jv()
self.update_time_sheet(None if (self.is_return and self.return_against) else self.name)
self.update_time_sheet(self.name)
if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
update_company_current_month_sales(self.company)
@@ -567,7 +550,7 @@ class SalesInvoice(SellingController):
self.check_if_consolidated_invoice()
super().before_cancel()
self.update_time_sheet(self.return_against if (self.is_return and self.return_against) else None)
self.update_time_sheet(None)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
@@ -752,20 +735,8 @@ class SalesInvoice(SellingController):
for data in timesheet.time_logs:
if (
(self.project and args.timesheet_detail == data.name)
or (not self.project and not data.sales_invoice and args.timesheet_detail == data.name)
or (
not sales_invoice
and data.sales_invoice == self.name
and args.timesheet_detail == data.name
)
or (
self.is_return
and self.return_against
and data.sales_invoice
and data.sales_invoice == self.return_against
and not sales_invoice
and args.timesheet_detail == data.name
)
or (not self.project and not data.sales_invoice)
or (not sales_invoice and data.sales_invoice == self.name)
):
data.sales_invoice = sales_invoice
@@ -791,13 +762,6 @@ class SalesInvoice(SellingController):
def set_paid_amount(self):
paid_amount = 0.0
base_paid_amount = 0.0
if not cint(self.is_pos) and self.is_return:
self.set("payments", [])
self.paid_amount = paid_amount
self.base_paid_amount = base_paid_amount
return
for data in self.payments:
data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount"))
paid_amount += data.amount
@@ -812,25 +776,11 @@ class SalesInvoice(SellingController):
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self):
# Note: This validation is skipped for return invoices
# to allow returns to reference already-billed timesheet details
for data in self.timesheets:
# Handle invoice duplication
if data.time_sheet and data.timesheet_detail:
if sales_invoice := frappe.db.get_value(
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
):
frappe.throw(
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
)
)
if data.time_sheet:
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
if status not in ["Submitted", "Payslip", "Partially Billed"]:
frappe.throw(
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
)
if status not in ["Submitted", "Payslip"]:
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
@@ -854,9 +804,11 @@ class SalesInvoice(SellingController):
if self.pos_profile:
pos = frappe.get_doc("POS Profile", self.pos_profile)
if not self.get("payments") and not for_validate:
update_multi_mode_option(self, pos)
if pos:
if not for_validate:
update_multi_mode_option(self, pos)
self.tax_category = pos.get("tax_category")
if not for_validate and not self.customer:
@@ -1162,12 +1114,7 @@ class SalesInvoice(SellingController):
timesheet.billing_amount = ts_doc.total_billable_amount
def update_timesheet_billing_for_project(self):
if (
not self.is_return
and not self.timesheets
and self.project
and self.is_auto_fetch_timesheet_enabled()
):
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
self.add_timesheet_data()
else:
self.calculate_billing_amount_for_timesheet()
@@ -1252,9 +1199,6 @@ class SalesInvoice(SellingController):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def process_asset_depreciation(self):
if self.is_internal_transfer():
return
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale()
else:
@@ -2538,7 +2482,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"rate": "rate",
},
"postprocess": update_item,
"condition": lambda doc: doc.qty - received_items.get(doc.name, 0.0) > 0,
"condition": lambda doc: doc.qty > 0,
}
if doctype in ["Sales Invoice", "Sales Order"]:
@@ -2571,25 +2515,18 @@ 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": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"],
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"],
},
doctype + " Item": item_field_map,
},
target_doc,
set_missing_values,
)
if not doclist.get("items"):
frappe.throw(
_(
"Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. "
"Please check the existing linked {2}s."
).format(target_doctype, doctype, target_doctype)
)
return doclist
@frappe.whitelist()
def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
def get_received_items(reference_name, doctype, reference_fieldname):
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
@@ -2602,19 +2539,20 @@ def get_received_items(reference_name: str, doctype: str, reference_fieldname: s
target_doctypes = frappe.get_all(
doctype,
filters=filters,
pluck="name",
as_list=True,
)
received_items_map = {}
if target_doctypes:
received_items_data = frappe.get_all(
target_doctypes = list(target_doctypes[0])
received_items_map = frappe._dict(
frappe.get_all(
doctype + " Item",
filters={"parent": ("in", target_doctypes)},
fields=[reference_fieldname, "qty"],
as_list=1,
)
for item in received_items_data:
key = item.get(reference_fieldname)
if key:
received_items_map[key] = received_items_map.get(key, 0.0) + flt(item.qty)
)
return received_items_map
@@ -2806,8 +2744,6 @@ def update_multi_mode_option(doc, pos_profile):
payment.account = payment_mode.default_account
payment.type = payment_mode.type
mop_refetched = bool(doc.payments)
doc.set("payments", [])
invalid_modes = []
mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")]
@@ -2829,11 +2765,6 @@ def update_multi_mode_option(doc, pos_profile):
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
if mop_refetched:
frappe.msgprint(
_("Payment methods refreshed. Please review before proceeding."), indicator="orange", alert=True
)
def get_all_mode_of_payments(doc):
return frappe.db.sql(

View File

@@ -1049,21 +1049,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(pos_return.get("payments")[0].amount, -500)
self.assertEqual(pos_return.get("payments")[1].amount, -500)
def test_non_pos_return_clears_payment_rows(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
si = create_sales_invoice(do_not_save=True)
si.append("payments", {"mode_of_payment": "Cash", "amount": 100})
si.insert()
si.submit()
si_return = make_sales_return(si.name)
si_return.insert()
self.assertEqual(si_return.is_pos, 0)
self.assertEqual(si_return.get("payments"), [])
self.assertEqual(si_return.paid_amount, 0)
def test_pos_change_amount(self):
make_pos_profile(
company="_Test Company with perpetual inventory",
@@ -2705,95 +2690,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
def test_restrict_inter_company_pi_when_sales_invoice_qty_fully_consumed(self):
item_code_1 = "_Test IC Item 1"
item_code_2 = "_Test IC Item 2"
create_item(item_code_1, is_stock_item=1)
create_item(item_code_2, is_stock_item=1)
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
item_code=item_code_1,
debit_to="Debtors - WP",
warehouse="Stores - WP",
income_account="Sales - WP",
expense_account="Cost of Goods Sold - WP",
cost_center="Main - WP",
currency="USD",
qty=3,
do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.append(
"items",
{
"item_code": item_code_2,
"item_name": item_code_2,
"description": item_code_2,
"warehouse": "Stores - WP",
"qty": 2,
"uom": "Nos",
"stock_uom": "Nos",
"rate": 100,
"price_list_rate": 100,
"income_account": "Sales - WP",
"expense_account": "Cost of Goods Sold - WP",
"cost_center": "Main - WP",
"conversion_factor": 1,
},
)
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
for item in target_doc.items:
item.update(
{
"expense_account": "Cost of Goods Sold - _TC1",
"cost_center": "Main - _TC1",
}
)
target_doc.submit()
self.assertEqual(len(target_doc.items), 2)
self.assertEqual([item.qty for item in target_doc.items], [3, 2])
with self.assertRaisesRegex(
frappe.ValidationError,
"already been fully invoiced",
):
make_inter_company_transaction("Sales Invoice", si.name)
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
@@ -3021,7 +2917,7 @@ class TestSalesInvoice(FrappeTestCase):
si.submit()
# Check if adjustment entry is created
self.assertFalse(
self.assertTrue(
frappe.db.exists(
"GL Entry",
{
@@ -3334,7 +3230,7 @@ class TestSalesInvoice(FrappeTestCase):
calculate_depreciation=1,
submit=1,
)
post_depreciation_entries(date="2025-04-01")
post_depreciation_entries()
si = create_sales_invoice(
item_code="Macbook Pro", asset=asset.name, qty=1, rate=10000, posting_date=getdate("2025-05-01")
@@ -3700,51 +3596,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertTrue("cannot overbill" in str(err.exception).lower())
dn.cancel()
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_so_is_blocked(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
service_item = create_item(
"_Test Service Item Non Stock SI",
is_stock_item=0,
).name
so = make_sales_order(item_code=service_item, qty=5, rate=100)
so.submit()
si = make_si_from_so(so.name)
si.items[0].qty = 10 # overbill by 100 %
si.save()
with self.assertRaises(frappe.ValidationError):
si.submit()
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_non_stock_item_over_billing_against_so_from_quotation_is_blocked(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order as make_so_from_quotation
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice as make_si_from_so
service_item = create_item(
"_Test Service Item Non Stock SI Quot",
is_stock_item=0,
).name
quotation = make_quotation(item_code=service_item, qty=5, rate=100)
so = make_so_from_quotation(quotation.name)
so.delivery_date = frappe.utils.add_days(frappe.utils.today(), 7)
so.insert()
so.submit()
si = make_si_from_so(so.name)
si.items[0].qty = 10 # overbill by 100 %
si.save()
with self.assertRaises(frappe.ValidationError):
si.submit()
@change_settings(
"Accounts Settings",
{
@@ -4984,33 +4835,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
def test_inter_company_transaction_cost_center(self):
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",
parent_cost_center="Main - WP",
cost_center="Main - WP",
currency="USD",
do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.submit()
cost_center = frappe.db.get_value("Company", "_Test Company 1", "cost_center")
frappe.db.set_value("Company", "_Test Company 1", "cost_center", None)
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
self.assertEqual(target_doc.cost_center, None)
self.assertEqual(target_doc.items[0].cost_center, None)
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -52,6 +52,7 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Timesheet Detail",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -116,7 +117,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-04-06 22:30:28.513139",
"modified": "2021-10-02 03:48:44.979777",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",

View File

@@ -25,10 +25,6 @@ frappe.ui.form.on("Shipping Rule", {
},
calculate_based_on: function (frm) {
frm.trigger("toggle_reqd");
if (frm.doc.calculate_based_on === "Fixed") {
frm.clear_table("conditions");
frm.refresh_field("conditions");
}
},
toggle_reqd: function (frm) {
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed");

View File

@@ -58,11 +58,6 @@ class ShippingRule(Document):
self.validate_overlapping_shipping_rule_conditions()
def validate_from_to_values(self):
if self.calculate_based_on == "Fixed":
if self.conditions:
self.set("conditions", [])
return
zero_to_values = []
for d in self.get("conditions"):
@@ -157,9 +152,7 @@ class ShippingRule(Document):
frappe.throw(_("Shipping rule only applicable for Buying"))
shipping_charge["doctype"] = "Purchase Taxes and Charges"
shipping_charge["category"] = (
"Valuation and Total" if doc.get_stock_items() or doc.get_asset_items() else "Total"
)
shipping_charge["category"] = "Valuation and Total"
shipping_charge["add_deduct_tax"] = "Add"
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)

View File

@@ -629,21 +629,18 @@ def create_parties():
customer.customer_name = "_Test Subscription Customer"
customer.default_currency = "USD"
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
customer.customer_group = "Individual"
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer Multi Currency"):
customer = frappe.new_doc("Customer")
customer.customer_name = "Test Subscription Customer Multi Currency"
customer.default_currency = "USD"
customer.customer_group = "Individual"
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer John Doe"
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable - _TC"})
customer.customer_group = "Individual"
customer.insert()

View File

@@ -8,13 +8,10 @@ import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_default_address
from frappe.model.document import Document
from frappe.query_builder import DocType
from frappe.query_builder.functions import IfNull
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):
@@ -177,44 +174,38 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
TaxRule = DocType("Tax Rule")
query = frappe.qb.from_(TaxRule).select("*")
conditions = []
if posting_date:
query = query.where(
(TaxRule.from_date.isnull() | (TaxRule.from_date <= posting_date))
& (TaxRule.to_date.isnull() | (TaxRule.to_date >= posting_date))
conditions.append(
f"""(from_date is null or from_date <= '{posting_date}')
and (to_date is null or to_date >= '{posting_date}')"""
)
else:
query = query.where(TaxRule.from_date.isnull() & TaxRule.to_date.isnull())
conditions.append("(from_date is null) and (to_date is null)")
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", "")
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"]
for key, value in args.items():
if key == "use_for_shopping_cart":
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))
)
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})")
else:
query = query.where(IfNull(TaxRule[key], "").isin(["", value or ""]))
conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})")
tax_rule = query.run(as_dict=True)
tax_rule = frappe.db.sql(
"""select * from `tabTax Rule`
where {}""".format(" and ".join(conditions)),
as_dict=True,
)
if not tax_rule:
return None
@@ -243,3 +234,11 @@ 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

View File

@@ -72,117 +72,6 @@ class TestTaxRule(unittest.TestCase):
"_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",

View File

@@ -697,17 +697,10 @@ def make_reverse_gl_entries(
partial_cancel=partial_cancel,
)
validate_accounting_period(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)
if immutable_ledger_enabled:
validation_date = posting_date or frappe.form_dict.get("posting_date") or getdate()
else:
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
check_freezing_date(validation_date, adv_adj)
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
validate_against_pcv(is_opening, gl_entries[0]["posting_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`
@@ -772,7 +765,7 @@ def make_reverse_gl_entries(
if immutable_ledger_enabled:
new_gle["is_cancelled"] = 0
new_gle["posting_date"] = posting_date or frappe.form_dict.get("posting_date") or getdate()
new_gle["posting_date"] = frappe.form_dict.get("posting_date") or getdate()
elif posting_date:
new_gle["posting_date"] = posting_date
@@ -804,24 +797,13 @@ def check_freezing_date(posting_date, adv_adj=False):
)
def validate_opening_entry_against_pcv(company):
if frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
def validate_against_pcv(is_opening, posting_date, company):
if is_opening and frappe.db.exists("Period Closing Voucher", {"docstatus": 1, "company": company}):
frappe.throw(
_(
"A Period Closing Voucher is already submitted and an Opening Entry can no longer be created. {0} to learn more."
).format(
'<a href="https://docs.frappe.io/erpnext/period-closing-voucher#14-pcv-and-opening-entries" target="_blank" rel="noopener">'
+ _("Read the docs")
+ "</a>"
),
_("Opening Entry can not be created after Period Closing Voucher is created."),
title=_("Invalid Opening Entry"),
)
def validate_against_pcv(is_opening, posting_date, company):
if is_opening:
validate_opening_entry_against_pcv(company)
# Local import so you don't have to touch file-level imports
from frappe.query_builder.functions import Max

View File

@@ -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\\\")\"], [\"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\"]]",
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Incoming Bills",
"modified": "2026-06-01 12:00:00.000000",
"modified": "2024-11-20 19:08:37.043777",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Incoming Bills",

View File

@@ -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\\\")\"], [\"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\"]]",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Incoming Payment",
"modified": "2026-06-01 12:00:00.000000",
"modified": "2020-07-22 13:06:20.237689",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Incoming Payment",

View File

@@ -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\\\")\"], [\"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\"]]",
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Outgoing Bills",
"modified": "2026-06-01 12:00:00.000000",
"modified": "2020-07-22 13:07:19.633101",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Outgoing Bills",

View File

@@ -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\\\")\"], [\"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\"]]",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Outgoing Payment",
"modified": "2026-06-01 12:00:00.000000",
"modified": "2020-07-22 12:49:34.942896",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Outgoing Payment",

View File

@@ -48,25 +48,6 @@ 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
@@ -431,17 +412,6 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group),
finally will return default."""
def account_perm_check(account):
ptype = "select" if frappe.only_has_select_perm("Account") else "read"
if frappe.has_permission("Account", ptype, account):
return
# Using custom message to prevent data leak in case of `apply_strict_permission` is enabled.
frappe.throw(
_("User don't have permissions to select/read this account."), exc=frappe.PermissionError
)
if not party_type:
frappe.throw(_("Party Type is mandatory"))
if not company:
@@ -452,51 +422,46 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
"default_receivable_account" if party_type == "Customer" else "default_payable_account"
)
account = frappe.get_cached_value("Company", company, default_account_name)
else:
return frappe.get_cached_value("Company", company, default_account_name)
account = frappe.db.get_value(
"Party Account", {"parenttype": party_type, "parent": party, "company": company}, "account"
)
if not account and party_type in ["Customer", "Supplier"]:
party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group"
group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype))
account = frappe.db.get_value(
"Party Account", {"parenttype": party_type, "parent": party, "company": company}, "account"
"Party Account",
{"parenttype": party_group_doctype, "parent": group, "company": company},
"account",
)
if not account and party_type in ["Customer", "Supplier"]:
party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group"
group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype))
account = frappe.db.get_value(
"Party Account",
{"parenttype": party_group_doctype, "parent": group, "company": company},
"account",
)
if not account and party_type in ["Customer", "Supplier"]:
default_account_name = (
"default_receivable_account" if party_type == "Customer" else "default_payable_account"
)
account = frappe.get_cached_value("Company", company, default_account_name)
if not account and party_type in ["Customer", "Supplier"]:
default_account_name = (
"default_receivable_account" if party_type == "Customer" else "default_payable_account"
)
account = frappe.get_cached_value("Company", company, default_account_name)
existing_gle_currency = get_party_gle_currency(party_type, party, company)
if existing_gle_currency:
if account:
account_currency = frappe.get_cached_value("Account", account, "account_currency")
if (account and account_currency != existing_gle_currency) or not account:
account = get_party_gle_account(party_type, party, company)
existing_gle_currency = get_party_gle_currency(party_type, party, company)
if existing_gle_currency:
if account:
account_currency = frappe.get_cached_value("Account", account, "account_currency")
if (account and account_currency != existing_gle_currency) or not account:
account = get_party_gle_account(party_type, party, company)
# get default account on the basis of party type
if not account:
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
default_account_name = "default_" + account_type.lower() + "_account"
account = frappe.get_cached_value("Company", company, default_account_name)
# get default account on the basis of party type
if not account:
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
default_account_name = "default_" + account_type.lower() + "_account"
account = frappe.get_cached_value("Company", company, default_account_name)
if account:
account_perm_check(account)
if include_advance and party and party_type in ["Customer", "Supplier", "Student"]:
if include_advance and party_type in ["Customer", "Supplier", "Student"]:
advance_account = get_party_advance_account(party_type, party, company)
if advance_account:
account_perm_check(advance_account)
return [account, advance_account]
return [account]
else:
return [account]
return account
@@ -526,6 +491,11 @@ def get_party_advance_account(party_type, party, company):
return account
@frappe.whitelist()
def get_party_bank_account(party_type, party):
return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1})
def get_party_account_currency(party_type, party, company):
def generator():
party_account = get_party_account(party_type, party, company)
@@ -560,19 +530,11 @@ def get_party_gle_currency(party_type, party, company):
def get_party_gle_account(party_type, party, company):
def generator():
gl = qb.DocType("GL Entry")
existing_gle_account = (
qb.from_(gl)
.select(gl.account)
.where(
(gl.docstatus == 1)
& (gl.company == company)
& (gl.party_type == party_type)
& (gl.party == party)
& (gl.is_cancelled == 0)
)
.limit(1)
.run()
existing_gle_account = frappe.db.sql(
"""select account from `tabGL Entry`
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
limit 1""",
{"company": company, "party_type": party_type, "party": party},
)
return existing_gle_account[0][0] if existing_gle_account else None
@@ -707,7 +669,7 @@ def validate_due_date_with_template(posting_date, due_date, bill_date, template_
if not default_due_date:
return
if getdate(default_due_date) != getdate(posting_date) and getdate(due_date) > getdate(default_due_date):
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
@@ -781,7 +743,7 @@ def set_taxes(
args.update({"tax_type": "Purchase"})
if use_for_shopping_cart:
args.update({"use_for_shopping_cart": cint(use_for_shopping_cart)})
args.update({"use_for_shopping_cart": use_for_shopping_cart})
return get_tax_template(posting_date, args)
@@ -938,15 +900,6 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
if party_type == "Supplier":
info["total_unpaid"] = -1 * info["total_unpaid"]
if info["total_unpaid"] < 0:
info["balance_label"] = (
"Total Advance Paid" if party_type == "Supplier" else "Total Advance Received"
)
info["balance_amount"] = abs(info["total_unpaid"])
else:
info["balance_label"] = "Total Unpaid"
info["balance_amount"] = info["total_unpaid"]
company_wise_info.append(info)
return company_wise_info

View File

@@ -0,0 +1,23 @@
{
"align_labels_right": 0,
"creation": "2016-05-05 17:16:18.564460",
"custom_format": 1,
"disabled": 0,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n<p class=\"text-center\">\n\t{{ company }}<br>\n\t{{ __(\"POS No : \") }} {{ offline_pos_name }}<br>\n</p>\n<p>\n\t<b>{{ __(\"Customer\") }}:</b> {{ customer }}<br>\n</p>\n\n<p>\n\t<b>{{ __(\"Date\") }}:</b> {{ dateutil.global_date_format(posting_date) }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ __(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{% for item in items %}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_name }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ format_number(item.qty, null,precision(\"difference\")) }}<br>@ {{ format_currency(item.rate, currency) }}</td>\n\t\t\t<td class=\"text-right\">{{ format_currency(item.amount, currency) }}</td>\n\t\t</tr>\n\t\t{% endfor %}\n\t</tbody>\n</table>\n\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ row.description }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Qty Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ qty_total }}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n\n\n<hr>\n<p>{{ terms }}</p>\n<p class=\"text-center\">{{ __(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"modified": "2019-09-05 17:20:30.726659",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Point of Sale",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "JS",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@@ -34,17 +34,6 @@ frappe.query_reports["Accounts Payable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_account",
label: __("Payable Account"),

View File

@@ -120,49 +120,3 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 300.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,17 +53,6 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

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