Compare commits

..

1 Commits

Author SHA1 Message Date
Ankush Menat
1475849295 ci: Rebuild mysqlclient 2025-06-26 16:25:07 +05:30
599 changed files with 172174 additions and 466015 deletions

View File

@@ -1,12 +0,0 @@
reviews:
auto_review:
ignore_title_keywords:
- "sync translations"
- "update POT file"
- "style: "
review_status: false
poem: false
collapse_walkthrough: true
sequence_diagrams: false
changed_files_summary: false
high_level_summary: false

View File

@@ -4,10 +4,14 @@ set -e
cd ~ || exit
curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash
sudo apt update
sudo apt remove mysql-server mysql-client
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev
pip cache remove mysqlclient
pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}

View File

@@ -8,9 +8,6 @@ on:
- '**.md'
- '**.html'
- '**.csv'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
workflow_dispatch:
permissions:
@@ -85,7 +82,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

@@ -10,9 +10,6 @@ on:
- "**.md"
- "**.html"
- "**.csv"
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
permissions:
contents: read

View File

@@ -111,7 +111,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

@@ -9,9 +9,6 @@ on:
- "**.css"
- "**.md"
- "**.html"
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
permissions:
contents: read

View File

@@ -9,9 +9,6 @@ on:
- '**.css'
- '**.md'
- '**.html'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"
@@ -109,7 +106,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

@@ -6,9 +6,6 @@ on:
- '**.js'
- '**.md'
- '**.html'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
types: [opened, labelled, synchronize, reopened]
concurrency:
@@ -94,7 +91,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
id: yarn-cache

View File

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

View File

@@ -8,16 +8,17 @@ erpnext/assets/ @khushi8112
erpnext/regional @ruthra-kumar
erpnext/selling @ruthra-kumar
erpnext/support/ @ruthra-kumar
pos*
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
erpnext/buying/ @rohitwaghchaure
erpnext/maintenance/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
erpnext/manufacturing/ @rohitwaghchaure
erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
erpnext/subcontracting @mihir-kandoi
erpnext/stock/ @rohitwaghchaure
erpnext/subcontracting @rohitwaghchaure
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
erpnext/patches/ @ruthra-kumar
.github/ @ruthra-kumar
pyproject.toml @ruthra-kumar
pyproject.toml @akhilnarang

View File

@@ -10,10 +10,8 @@ from frappe.contacts.doctype.address.address import (
class ERPNextAddress(Address):
def validate(self):
self.validate_reference()
self.update_company_address()
if hasattr(super(), "validate"):
super().validate()
self.update_compnay_address()
super().validate()
def link_address(self):
"""Link address based on owner"""
@@ -22,7 +20,7 @@ class ERPNextAddress(Address):
return super().link_address()
def update_company_address(self):
def update_compnay_address(self):
for link in self.get("links"):
if link.link_doctype == "Company":
self.is_your_company_address = 1
@@ -40,10 +38,6 @@ class ERPNextAddress(Address):
"""
After Address is updated, update the related 'Primary Address' on Customer.
"""
if hasattr(super(), "on_update"):
super().on_update()
address_display = get_address_display(self.as_dict())
filters = {"customer_primary_address": self.name}
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)

View File

@@ -46,8 +46,7 @@ def validate_service_stop_date(doc):
if (
old_stop_dates
and old_stop_dates.get(item.name)
and item.service_stop_date
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
and item.service_stop_date != old_stop_dates.get(item.name)
):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))

View File

@@ -167,7 +167,7 @@ class Account(NestedSet):
if par.root_type:
self.root_type = par.root_type
if cint(self.is_group):
if self.is_group:
db_value = self.get_doc_before_save()
if db_value:
if self.report_type != db_value.report_type:
@@ -210,7 +210,7 @@ class Account(NestedSet):
if doc_before_save and not doc_before_save.parent_account:
throw(_("Root cannot be edited."), RootNotEditable)
if not self.parent_account and not cint(self.is_group):
if not self.parent_account and not self.is_group:
throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
def validate_root_company_and_sync_account_to_children(self):
@@ -259,7 +259,7 @@ class Account(NestedSet):
if self.check_gle_exists():
throw(_("Account with existing transaction cannot be converted to ledger"))
elif cint(self.is_group):
elif self.is_group:
if self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected."))
elif self.check_if_child_exists():
@@ -302,9 +302,7 @@ class Account(NestedSet):
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
self.currency_explicitly_specified = False
gl_currency = frappe.db.get_value(
"GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency"
)
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
if gl_currency and self.account_currency != gl_currency:
if frappe.db.get_value("GL Entry", {"account": self.name}):

View File

@@ -270,14 +270,12 @@ frappe.treeview_settings["Account"] = {
label: __("View Ledger"),
click: function (node, btn) {
frappe.route_options = {
account: node.label,
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
company:
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
};
if (node.parent_label) {
frappe.route_options["account"] = node.label;
}
frappe.set_route("query-report", "General Ledger");
},
btnClass: "hidden-xs",

View File

@@ -18,7 +18,6 @@ def create_charts(
accounts = []
def _import_accounts(children, parent, root_type, root_account=False):
nonlocal custom_chart
for account_name, child in children.items():
if root_account:
root_type = child.get("root_type")
@@ -56,8 +55,7 @@ def create_charts(
"account_number": account_number,
"account_type": child.get("account_type"),
"account_currency": child.get("account_currency")
if custom_chart
else frappe.get_cached_value("Company", company, "default_currency"),
or frappe.get_cached_value("Company", company, "default_currency"),
"tax_rate": child.get("tax_rate"),
}
)

View File

@@ -1,817 +0,0 @@
{
"country_code": "au",
"name": "Australia - Chart of Accounts with Account Numbers",
"tree": {
"Assets": {
"Current Assets": {
"Cash On Hand": {
"Cash On Hand": {
"account_number": "11010",
"account_type": "Cash"
},
"account_number": "110",
"is_group": 1
},
"Cash at Bank": {
"Every Day Bank Account": {
"account_number": "11510",
"account_type": "Bank"
},
"Business Savings Account": {
"account_number": "11520"
},
"Business Term Deposit": {
"account_number": "11530"
},
"account_number": "115",
"is_group": 1
},
"Trade Receivables": {
"Trade Debtors": {
"account_number": "12010",
"account_type": "Receivable"
},
"Provision for Doubtful Debts": {
"account_number": "12020"
},
"Sundry Debtors": {
"account_number": "12030"
},
"Debtor Refund": {
"account_number": "12040"
},
"account_number": "120",
"is_group": 1
},
"Inventory": {
"Stock On Hand": {
"account_number": "13010",
"account_type": "Stock"
},
"WIP - Work In Progress - Manufacturing": {
"account_number": "13020"
},
"account_number": "130",
"is_group": 1
},
"Prepayments": {
"Prepayments": {
"account_number": "14010"
},
"Provisional Tax Paid": {
"account_number": "14020"
},
"account_number": "140",
"is_group": 1
},
"account_number": "11",
"is_group": 1
},
"Non Current Assets": {
"Plant & Equipment": {
"Plant & Equipment": {
"account_number": "16010",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation Plant & Equipment": {
"account_number": "16020",
"account_type": "Accumulated Depreciation"
},
"account_number": "160",
"is_group": 1
},
"Motor Vehicle": {
"Motor Vehicle": {
"account_number": "16110",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation Motor Vehicle": {
"account_number": "16120",
"account_type": "Accumulated Depreciation"
},
"account_number": "161",
"is_group": 1
},
"Office Equipment": {
"Office Furniture & Equipment": {
"account_number": "16210",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation Office Furniture & Equipment": {
"account_number": "16220",
"account_type": "Accumulated Depreciation"
},
"account_number": "162",
"is_group": 1
},
"Computer Equipment": {
"Computer Equipment": {
"account_number": "16310",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation Computer Equipment": {
"account_number": "16320",
"account_type": "Accumulated Depreciation"
},
"account_number": "163",
"is_group": 1
},
"Building": {
"Buildings": {
"account_number": "16410",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation Buildings": {
"account_number": "16420",
"account_type": "Accumulated Depreciation"
},
"CWIP - Construction Work In Progress": {
"account_number": "16430",
"account_type": "Capital Work in Progress"
},
"Accumulated Depreciation - Others": {
"account_number": "16440",
"account_type": "Accumulated Depreciation"
},
"account_number": "164",
"is_group": 1
},
"Related Party": {
"Loan to Party 1": {
"account_number": "17010"
},
"account_number": "170",
"is_group": 1
},
"Investments & Unlisted Entities": {
"Investment - Entity 1": {
"account_number": "17510"
},
"account_number": "175",
"is_group": 1
},
"Intagible Assets": {
"Goodwill": {
"account_number": "18010"
},
"Opening Balance Temporary ": {
"account_number": "18090",
"account_type": "Temporary"
},
"account_number": "180",
"is_group": 1
},
"account_number": "16",
"is_group": 1
},
"account_number": "1",
"root_type": "Asset"
},
"Liabilities": {
"Current Liabilities": {
"Trade Payables - Current": {
"Trade Creditors": {
"account_number": "21010",
"account_type": "Payable"
},
"Goods Received Not Invoiced": {
"account_number": "21050",
"account_type": "Stock Received But Not Billed"
},
"Service Received Not Invoiced": {
"account_number": "21060"
},
"Asset Received Not Invoiced": {
"account_number": "21070",
"account_type": "Asset Received But Not Billed"
},
"account_number": "210",
"is_group": 1
},
"Other Payables - Current": {
"Accrued Expenses": {
"account_number": "21510"
},
"Payroll - Wages Clearing": {
"account_number": "21550"
},
"Payroll - Superannuation Deductions": {
"account_number": "21555"
},
"Payroll - Misc Deductions": {
"account_number": "21560"
},
"Payroll - Withholding Tax Payable": {
"account_number": "21565"
},
"account_number": "215",
"is_group": 1
},
"GST": {
"GST Payments to ATO": {
"account_number": "22030"
},
"Provision for PAYG Tax": {
"account_number": "22040"
},
"account_number": "220",
"account_type": "Tax",
"is_group": 1
},
"Interest & Non Bearing Liabilities - Current": {
"Credit Card - VISA": {
"account_number": "22510"
},
"account_number": "225",
"is_group": 1
},
"Bank Overdraft": {
"Bank Overdraft Cash at Bank": {
"account_number": "23010"
},
"account_number": "230",
"is_group": 1
},
"Trade Finance": {
"Trade Finance": {
"account_number": "23510"
},
"account_number": "235",
"is_group": 1
},
"Lease Liabilities": {
"Finance Lease - Current": {
"account_number": "24010"
},
"account_number": "240",
"is_group": 1
},
"Provisions": {
"Provision for Long Service Leave": {
"account_number": "24510"
},
"Provision for Holiday Pay": {
"account_number": "24520"
},
"account_number": "245",
"is_group": 1
},
"account_number": "21",
"is_group": 1
},
"Non Current Liabilities": {
"Trade & Other Payables - Non Current": {
"Loan Account - Party 1": {
"account_number": "25010"
},
"account_number": "250",
"is_group": 1
},
"Interest & Non Bearing Liabilities - Non Current": {
"Non Current Liability - Director Loan": {
"account_number": "25510"
},
"account_number": "255",
"is_group": 1
},
"Bank Loans - Non Current": {
"Bank Loan 1 - Non Current": {
"account_number": "26010"
},
"account_number": "260",
"is_group": 1
},
"Lease Liabilities - Non Current": {
"Finance Lease - Non Current": {
"account_number": "27010"
},
"account_number": "270",
"is_group": 1
},
"Provisions - Non Current": {
"Provision for Long Service Leave": {
"account_number": "27510"
},
"Provision for Holiday Pay": {
"account_number": "27520"
},
"account_number": "275",
"is_group": 1
},
"account_number": "25",
"is_group": 1
},
"account_number": "2",
"root_type": "Liability"
},
"Equity": {
"Equity": {
"Owner's/Shareholder's Equity": {
"Owner's/Shareholders Capital": {
"account_number": "31010",
"account_type": "Equity"
},
"Owner's/Shareholders Drawings": {
"account_number": "31020",
"account_type": "Equity"
},
"account_number": "310",
"is_group": 1
},
"Earnings": {
"Current Year Earnings": {
"account_number": "35010",
"account_type": "Equity"
},
"Retained Earnings": {
"account_number": "35020",
"account_type": "Equity"
},
"account_number": "350",
"is_group": 1
},
"account_number": "31",
"is_group": 1
},
"account_number": "3",
"root_type": "Equity"
},
"Revenue": {
"Revenue": {
"Sales Revenue": {
"Sales Income": {
"account_number": "41010",
"account_type": "Income Account"
},
"Freight Income": {
"account_number": "41020",
"account_type": "Income Account"
},
"Other Income": {
"account_number": "41030",
"account_type": "Income Account"
},
"Service Income": {
"account_number": "41040",
"account_type": "Income Account"
},
"account_number": "410",
"is_group": 1
},
"Other Revenue": {
"Commission Received": {
"account_number": "42010"
},
"Discounts Received": {
"account_number": "42020"
},
"Interest received": {
"account_number": "42030"
},
"Profit/Loss on Sales of Assets": {
"account_number": "42040"
},
"Rent Received": {
"account_number": "42050"
},
"Sundry Income": {
"account_number": "42060"
},
"account_number": "420",
"is_group": 1
},
"account_number": "41",
"is_group": 1
},
"account_number": "4",
"root_type": "Income"
},
"Cost of Goods": {
"Cost of Goods": {
"Cost of Goods Sold": {
"Cost of Goods Sold": {
"account_number": "51010",
"account_type": "Cost of Goods Sold"
},
"Freight Expenses (sales related)": {
"account_number": "51020"
},
"Discounts Given": {
"account_number": "51030"
},
"Subcontracting Charges": {
"account_number": "51040"
},
"account_number": "510",
"is_group": 1
},
"Other COGS": {
"Purchases - Miscellaneous": {
"account_number": "52010"
},
"Duty & Customs Fees": {
"account_number": "52020",
"account_type": "Tax"
},
"Freight Inwards": {
"account_number": "52030",
"account_type": "Chargeable"
},
"Stock Adjustment": {
"account_number": "52040",
"account_type": "Stock Adjustment"
},
"Stock Wirte Off": {
"account_number": "52050",
"account_type": "Stock Adjustment"
},
"Stock Valuation Expenses": {
"account_number": "52060",
"account_type": "Expenses Included In Valuation"
},
"Asset Valuation Expenses": {
"account_number": "52070",
"account_type": "Expenses Included In Asset Valuation"
},
"account_number": "520",
"is_group": 1
},
"account_number": "51",
"is_group": 1
},
"account_number": "5",
"root_type": "Expense"
},
"Expenses": {
"Fixed Expenses": {
"Payroll & Related Expenses": {
"Salaries & Wages": {
"account_number": "61010"
},
"Superannuation": {
"account_number": "61015"
},
"Staff Amenities - GST Paid": {
"account_number": "61020"
},
"Staff Amenities - GST Free": {
"account_number": "61025"
},
"Staff Recruitment": {
"account_number": "61030"
},
"Staff Training": {
"account_number": "61035"
},
"Fringe Benefits Tax": {
"account_number": "61040"
},
"Payroll Tax": {
"account_number": "61045"
},
"Workers Compensation": {
"account_number": "61050"
},
"Long Service Leave": {
"account_number": "61060"
},
"Mileage Reimbursement": {
"account_number": "61070"
},
"Overtime": {
"account_number": "61080"
},
"Worksafe Insurance": {
"account_number": "61090"
},
"account_number": "610",
"is_group": 1
},
"Depreciation Expenses": {
"Depreciation - Plant & Equipment": {
"account_number": "62010",
"account_type": "Depreciation"
},
"Depreciation - Motor Vehicle": {
"account_number": "62020",
"account_type": "Depreciation"
},
"Depreciation - Office Equipment": {
"account_number": "62030",
"account_type": "Depreciation"
},
"Depreciation - Computer Equipment": {
"account_number": "62040",
"account_type": "Depreciation"
},
"Depreciation - Building": {
"account_number": "62050",
"account_type": "Depreciation"
},
"Depreciation - Others": {
"account_number": "62510",
"account_type": "Depreciation"
},
"account_number": "620",
"is_group": 1
},
"account_number": "61",
"is_group": 1
},
"Accrued Expenses": {
"Accrued Expenses": {
"Accrued Expenses - Salaries & Wages": {
"account_number": "63010"
},
"Accrued Expenses - Interest": {
"account_number": "63020"
},
"account_number": "630",
"is_group": 1
},
"account_number": "63",
"is_group": 1
},
"Operating Expenses": {
"General and Administrative Expenses": {
"Low Value Assets less than $300": {
"account_number": "64010"
},
"Office Supplies": {
"account_number": "64020"
},
"Postage & Courier": {
"account_number": "64025"
},
"Printing & Stationery": {
"account_number": "64030"
},
"Registration Fees / Filing Fees": {
"account_number": "64040"
},
"Travel & Accommodation - Local": {
"account_number": "64050"
},
"Travel & Accommodation - Overseas": {
"account_number": "64060"
},
"Relocation Costs": {
"account_number": "64070"
},
"Hire Charges": {
"account_number": "64080"
},
"Repairs & Maintenance": {
"account_number": "64210"
},
"Cleaning Expenses": {
"account_number": "64215"
},
"Uniforms": {
"account_number": "64220"
},
"Security": {
"account_number": "64225"
},
"Subscriptions & Licences": {
"account_number": "64510"
},
"Software Expenses": {
"account_number": "64515"
},
"Marketing Expenses": {
"account_number": "64520"
},
"Advertising Expenses": {
"account_number": "64525"
},
"Website Hosting & Domain Expenses": {
"account_number": "64530"
},
"Computer Repairs / Supplies": {
"account_number": "64540"
},
"Conferences": {
"account_number": "64550"
},
"Consultancy /Contract Services": {
"account_number": "64560"
},
"Training Services": {
"account_number": "64570"
},
"Workshop Supplies": {
"account_number": "64580"
},
"Consumables": {
"account_number": "64585"
},
"Entertainment Expenses - Deductible": {
"account_number": "64810"
},
"Entertainment Expenses - Non Deductible": {
"account_number": "64820"
},
"Amortisation Of Goodwill": {
"account_number": "64910"
},
"General / Miscellaneous Expenses": {
"account_number": "64915",
"account_type": "Chargeable"
},
"Donations": {
"account_number": "64920"
},
"Client Gifts": {
"account_number": "64930"
},
"Employee Gifts": {
"account_number": "64935"
},
"account_number": "640",
"is_group": 1
},
"Occupancy Expenses": {
"Rental Expenses": {
"account_number": "65010"
},
"Property Insurance": {
"account_number": "65020"
},
"Electricity Expenses": {
"account_number": "65030"
},
"Water Rates": {
"account_number": "65040"
},
"Gas Expenses": {
"account_number": "65050"
},
"Property Taxes": {
"account_number": "65060"
},
"Rates": {
"account_number": "65070"
},
"account_number": "650",
"is_group": 1
},
"Communication & Vehicle Expenses": {
"Internet Expenses": {
"account_number": "66010"
},
"Mobile Telephone": {
"account_number": "66020"
},
"Telephone Expenses": {
"account_number": "66030"
},
"Motor Vehicle - Fuel Expenses": {
"account_number": "66040"
},
"Motor Vehicle - Parking & Tolls": {
"account_number": "66050"
},
"Motor Vehicle - Registration & Insurance": {
"account_number": "66060"
},
"Motor Vehicle - Service & Repairs": {
"account_number": "66070"
},
"Taxi": {
"account_number": "66080"
},
"account_number": "660",
"is_group": 1
},
"account_number": "64",
"is_group": 1
},
"Non-Operating Expenses": {
"Finance Costs": {
"Interest - Bank Loans": {
"account_number": "67010"
},
"Interest - Finance Leases": {
"account_number": "67020"
},
"Interest - Other Loans": {
"account_number": "67025"
},
"Insurance": {
"account_number": "67030"
},
"Bank Charges": {
"account_number": "67050"
},
"Rounding off": {
"account_number": "67055",
"account_type": "Round Off"
},
"Audit Fees": {
"account_number": "67060"
},
"Accounting Fees": {
"account_number": "67070"
},
"Legal Fees": {
"account_number": "67080"
},
"Management Fees": {
"account_number": "67090"
},
"account_number": "670",
"is_group": 1
},
"Other Costs": {
"Doubtful Debts": {
"account_number": "67510"
},
"Fines": {
"account_number": "67520"
},
"Debt Collection": {
"account_number": "67530"
},
"Bad Debts": {
"account_number": "67540"
},
"account_number": "675",
"is_group": 1
},
"account_number": "67",
"is_group": 1
},
"Variable Expenses": {
"Variable Expenses": {
"Bonus & Commissions Paid": {
"account_number": "68010"
},
"Bonus & Commissions To be Paid": {
"account_number": "68020"
},
"Warranty Claims": {
"account_number": "68030"
},
"account_number": "680",
"is_group": 1
},
"account_number": "68",
"is_group": 1
},
"account_number": "6",
"root_type": "Expense"
},
"Other Income": {
"Other Income": {
"Interest Income": {
"Interest Income": {
"account_number": "71010"
},
"account_number": "710",
"is_group": 1
},
"Asset Disposal Income": {
"Gain on Asset Disposal": {
"account_number": "73010"
},
"account_number": "730",
"is_group": 1
},
"account_number": "71",
"is_group": 1
},
"account_number": "7",
"root_type": "Income"
},
"Other Expenses": {
"Other Expenses": {
"Income Tax Expenses": {
"Income Tax Expenses": {
"account_number": "81010"
},
"account_number": "810",
"is_group": 1
},
"Foreign Exchange Gain/Loss": {
"Exchange Loss/Gain - Realized": {
"account_number": "82010"
},
"account_number": "820",
"is_group": 1
},
"Asset Disposal Expenses": {
"Loss on Asset Disposal": {
"account_number": "83010"
},
"account_number": "830",
"is_group": 1
},
"account_number": "81",
"is_group": 1
},
"account_number": "8",
"root_type": "Expense"
}
}
}

View File

@@ -13,7 +13,7 @@ def get():
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
_("Loans and Advances (Assets)"): {
_("Employee Advances"): {"account_type": "Payable"},
_("Employee Advances"): {},
},
_("Securities and Deposits"): {_("Earnest Money"): {}},
_("Stock Assets"): {

View File

@@ -20,7 +20,7 @@ def get():
"account_number": "1100",
},
_("Loans and Advances (Assets)"): {
_("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
_("Employee Advances"): {"account_number": "1610"},
"account_number": "1600",
},
_("Securities and Deposits"): {

View File

@@ -11,9 +11,6 @@
"cost_center",
"debit",
"credit",
"reporting_currency_exchange_rate",
"debit_in_reporting_currency",
"credit_in_reporting_currency",
"account_currency",
"debit_in_account_currency",
"credit_in_account_currency",
@@ -127,30 +124,12 @@
"fieldname": "is_period_closing_voucher_entry",
"fieldtype": "Check",
"label": "Is Period Closing Voucher Entry"
},
{
"fieldname": "debit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "credit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "reporting_currency_exchange_rate",
"fieldtype": "Float",
"label": "Reporting Currency Exchange Rate",
"precision": "9"
}
],
"icon": "fa fa-list",
"in_create": 1,
"links": [],
"modified": "2025-08-22 19:13:50.400404",
"modified": "2024-03-27 13:05:56.710541",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Closing Balance",
@@ -179,8 +158,7 @@
"role": "Auditor"
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -2,15 +2,12 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, flt
from frappe.utils import cint, cstr
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
from erpnext.setup.utils import get_exchange_rate
class AccountClosingBalance(Document):
@@ -29,15 +26,12 @@ class AccountClosingBalance(Document):
cost_center: DF.Link | None
credit: DF.Currency
credit_in_account_currency: DF.Currency
credit_in_reporting_currency: DF.Currency
debit: DF.Currency
debit_in_account_currency: DF.Currency
debit_in_reporting_currency: DF.Currency
finance_book: DF.Link | None
is_period_closing_voucher_entry: DF.Check
period_closing_voucher: DF.Link | None
project: DF.Link | None
reporting_currency_exchange_rate: DF.Float
# end: auto-generated types
pass
@@ -61,7 +55,6 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
"closing_date": closing_date,
}
)
set_amount_in_reporting_currency(cle, company, closing_date)
cle.flags.ignore_permissions = True
cle.flags.ignore_links = True
cle.submit()
@@ -151,29 +144,3 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
entries = query.run(as_dict=1)
return entries
def set_amount_in_reporting_currency(cle, company, closing_date):
default_currency, reporting_currency = frappe.get_cached_value(
"Company", company, ["default_currency", "reporting_currency"]
)
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
if not reporting_currency_exchange_rate:
frappe.throw(
title=_("Reporting Currency Exchange Not Found"),
msg=_(
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
).format(default_currency, reporting_currency, closing_date),
exc=ReportingCurrencyExchangeNotFoundError,
)
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
cle.update(
{
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
"debit_in_reporting_currency": debit_in_reporting_currency,
"credit_in_reporting_currency": credit_in_reporting_currency,
}
)

View File

@@ -111,15 +111,17 @@ class AccountingDimension(Document):
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
doclist = get_doctypes_with_dimensions()
doc_count = len(get_accounting_dimensions())
count = 0
repostable_doctypes = get_allowed_types_from_settings(child_doc=True)
repostable_doctypes = get_allowed_types_from_settings()
for doctype in doclist:
if (doc_count + 1) % 2 == 0:
insert_after_field = "dimension_col_break"
else:
insert_after_field = "accounting_dimensions_section"
df = {
"fieldname": doc.fieldname,
"label": doc.label,

View File

@@ -7,7 +7,6 @@
"engine": "InnoDB",
"field_order": [
"accounting_dimension",
"fieldname",
"disabled",
"column_break_2",
"company",
@@ -91,17 +90,11 @@
"fieldname": "apply_restriction_on_values",
"fieldtype": "Check",
"label": "Apply restriction on dimension values"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-08 14:13:22.203011",
"modified": "2024-03-27 13:05:57.199186",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension Filter",
@@ -146,9 +139,8 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,16 +17,17 @@ class AccountingDimensionFilter(Document):
from frappe.types import DF
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
ApplicableOnAccount,
)
accounting_dimension: DF.Literal[None]
accounting_dimension: DF.Literal
accounts: DF.Table[ApplicableOnAccount]
allow_or_restrict: DF.Literal["Allow", "Restrict"]
apply_restriction_on_values: DF.Check
company: DF.Link
dimensions: DF.Table[AllowedDimension]
disabled: DF.Check
fieldname: DF.Data | None
# end: auto-generated types
def before_save(self):
@@ -36,10 +37,6 @@ class AccountingDimensionFilter(Document):
self.set("dimensions", [])
def validate(self):
self.fieldname = frappe.db.get_value(
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
self.validate_applicable_accounts()
def validate_applicable_accounts(self):
@@ -74,7 +71,7 @@ def get_dimension_filter_map():
"""
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, p.fieldname, a.is_mandatory
p.allow_or_restrict, a.is_mandatory
FROM
`tabApplicable On Account` a,
`tabAccounting Dimension Filter` p
@@ -89,6 +86,8 @@ def get_dimension_filter_map():
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
build_map(
dimension_filter_map,
f.fieldname,

View File

@@ -11,7 +11,6 @@
"end_date",
"column_break_4",
"company",
"disabled",
"section_break_7",
"closed_documents"
],
@@ -50,13 +49,6 @@
"options": "Company",
"reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Disabled"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
@@ -70,11 +62,10 @@
}
],
"links": [],
"modified": "2025-10-06 15:00:15.568067",
"modified": "2024-03-27 13:05:57.388109",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Period",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -114,9 +105,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -28,7 +28,6 @@ class AccountingPeriod(Document):
closed_documents: DF.Table[ClosedDocument]
company: DF.Link
disabled: DF.Check
end_date: DF.Date
period_name: DF.Data
start_date: DF.Date
@@ -117,7 +116,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
& (ap.disabled == 0)
& (cd.closed == 1)
& (cd.document_type == doc.doctype)
& (date >= ap.start_date)

View File

@@ -26,20 +26,9 @@ frappe.ui.form.on("Accounts Settings", {
add_taxes_from_taxes_and_charges_template(frm) {
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
},
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

@@ -42,7 +42,6 @@
"show_payment_schedule_in_print",
"item_price_settings_section",
"maintain_same_internal_transaction_rate",
"fetch_valuation_rate_for_internal_transaction",
"column_break_feyo",
"maintain_same_rate_action",
"role_to_override_stop_action",
@@ -91,14 +90,12 @@
"receivable_payable_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"payment_request_settings",
"create_pr_in_draft_status",
"budget_settings",
"use_legacy_budget_controller"
"use_new_budget_controller"
],
"fields": [
{
@@ -559,7 +556,7 @@
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
"options": "Buffered Cursor\nUnBuffered Cursor"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
@@ -598,6 +595,12 @@
"fieldtype": "Tab Break",
"label": "Budget"
},
{
"default": "1",
"fieldname": "use_new_budget_controller",
"fieldtype": "Check",
"label": "Use New Budget Controller"
},
{
"default": "1",
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
@@ -628,29 +631,6 @@
"fieldname": "add_taxes_from_taxes_and_charges_template",
"fieldtype": "Check",
"label": "Automatically Add Taxes from Taxes and Charges Template"
},
{
"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",
"fieldtype": "Check",
"label": "Fetch Valuation Rate for Internal Transaction"
},
{
"default": "0",
"fieldname": "use_legacy_budget_controller",
"fieldtype": "Check",
"label": "Use Legacy Budget Controller"
}
],
"grid_page_length": 50,
@@ -659,7 +639,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-09-24 16:08:08.515254",
"modified": "2025-06-23 15:55:33.346398",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -49,7 +49,6 @@ class AccountsSettings(Document):
enable_immutable_ledger: DF.Check
enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_valuation_rate_for_internal_transaction: DF.Check
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check
@@ -60,7 +59,7 @@ class AccountsSettings(Document):
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
@@ -74,7 +73,7 @@ class AccountsSettings(Document):
submit_journal_entries: DF.Check
unlink_advance_payment_on_cancelation_of_order: DF.Check
unlink_payment_on_cancellation_of_invoice: DF.Check
use_legacy_budget_controller: DF.Check
use_new_budget_controller: DF.Check
# end: auto-generated types
def validate(self):
@@ -150,16 +149,8 @@ class AccountsSettings(Document):
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
frappe.throw(
_("You cannot enable both the settings '{0}' and '{1}'.").format(
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
),
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

@@ -12,8 +12,7 @@
"against_voucher_no",
"amount",
"currency",
"event",
"delinked"
"event"
],
"fields": [
{
@@ -69,20 +68,12 @@
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"default": "0",
"fieldname": "delinked",
"fieldtype": "Check",
"label": "DeLinked",
"read_only": 1
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-10-13 15:11:58.300836",
"modified": "2024-11-05 10:31:28.736671",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Payment Ledger Entry",
@@ -116,8 +107,7 @@
"share": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -1,11 +1,9 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
# import frappe
from frappe.model.document import Document
from erpnext.accounts.utils import get_advance_payment_doctypes, update_voucher_outstanding
class AdvancePaymentLedgerEntry(Document):
# begin: auto-generated types
@@ -21,28 +19,9 @@ class AdvancePaymentLedgerEntry(Document):
amount: DF.Currency
company: DF.Link | None
currency: DF.Link | None
delinked: DF.Check
event: DF.Data | None
voucher_no: DF.DynamicLink | None
voucher_type: DF.Link | None
# end: auto-generated types
def on_update(self):
if (
self.against_voucher_type in get_advance_payment_doctypes()
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
def on_doctype_update():
frappe.db.add_index(
"Advance Payment Ledger Entry",
["against_voucher_type", "against_voucher_no"],
)
frappe.db.add_index(
"Advance Payment Ledger Entry",
["voucher_type", "voucher_no"],
)
pass

View File

@@ -17,7 +17,6 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_8",
"rate",
"section_break_9",
@@ -96,13 +95,6 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"

View File

@@ -132,8 +132,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "IBAN",
"length": 34,
"options": "IBAN"
"length": 30
},
{
"fieldname": "column_break_12",
@@ -209,7 +208,6 @@
"label": "Disabled"
}
],
"grid_page_length": 50,
"links": [
{
"group": "Transactions",
@@ -252,7 +250,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2025-08-29 12:32:01.081687",
"modified": "2024-10-30 09:41:14.113414",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@@ -284,10 +282,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "bank,account",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -52,6 +52,7 @@ class BankAccount(Document):
def validate(self):
self.validate_company()
self.validate_iban()
self.validate_account()
self.update_default_bank_account()
@@ -71,6 +72,35 @@ class BankAccount(Document):
if self.is_company_account and not self.company:
frappe.throw(_("Company is mandatory for company account"))
def validate_iban(self):
"""
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
"""
# IBAN field is optional
if not self.iban:
return
def encode_char(c):
# Position in the alphabet (A=1, B=2, ...) plus nine
return str(9 + ord(c) - 64)
# remove whitespaces, upper case to get the right number from ord()
iban = "".join(self.iban.split(" ")).upper()
# Move country code and checksum from the start to the end
flipped = iban[4:] + iban[:4]
# Encode characters as numbers
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
try:
to_check = int("".join(encoded))
except ValueError:
frappe.throw(_("IBAN is not valid"))
if to_check % 97 != 1:
frappe.throw(_("IBAN is not valid"))
def update_default_bank_account(self):
if self.is_default and not self.disabled:
frappe.db.set_value(
@@ -79,7 +109,6 @@ class BankAccount(Document):
"party_type": self.party_type,
"party": self.party,
"is_company_account": self.is_company_account,
"company": self.company,
"is_default": 1,
"disabled": 0,
},
@@ -88,6 +117,15 @@ class BankAccount(Document):
)
@frappe.whitelist()
def make_bank_account(doctype, docname):
doc = frappe.new_doc("Bank Account")
doc.party_type = doctype
doc.party = docname
return doc
def get_party_bank_account(party_type, party):
return frappe.db.get_value(
"Bank Account",

View File

@@ -8,4 +8,38 @@ from frappe.tests import IntegrationTestCase
class TestBankAccount(IntegrationTestCase):
pass
def test_validate_iban(self):
valid_ibans = [
"GB82 WEST 1234 5698 7654 32",
"DE91 1000 0000 0123 4567 89",
"FR76 3000 6000 0112 3456 7890 189",
]
invalid_ibans = [
# wrong checksum (3rd place)
"GB72 WEST 1234 5698 7654 32",
"DE81 1000 0000 0123 4567 89",
"FR66 3000 6000 0112 3456 7890 189",
]
bank_account = frappe.get_doc({"doctype": "Bank Account"})
try:
bank_account.validate_iban()
except AttributeError:
msg = "BankAccount.validate_iban() failed for empty IBAN"
self.fail(msg=msg)
for iban in valid_ibans:
bank_account.iban = iban
try:
bank_account.validate_iban()
except ValidationError:
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
self.fail(msg=msg)
for not_iban in invalid_ibans:
bank_account.iban = not_iban
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
with self.assertRaises(ValidationError, msg=msg):
bank_account.validate_iban()

View File

@@ -89,64 +89,46 @@ class BankClearance(Document):
@frappe.whitelist()
def update_clearance_date(self):
invalid_document = []
invalid_cheque_date = []
entries_to_update = []
def validate_entry(d):
is_valid = True
if not d.payment_document:
invalid_document.append(str(d.idx))
is_valid = False
if d.clearance_date and d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
invalid_cheque_date.append(str(d.idx))
is_valid = False
return is_valid
clearance_date_updated = False
for d in self.get("payment_entries"):
if validate_entry(d) and (d.clearance_date or self.include_reconciled_entries):
if d.clearance_date:
if not d.payment_document:
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
frappe.throw(
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
d.idx,
get_link_to_form(d.payment_document, d.payment_entry),
d.clearance_date,
d.cheque_date,
)
)
if d.clearance_date or self.include_reconciled_entries:
if not d.clearance_date:
d.clearance_date = None
entries_to_update.append(d)
if d.payment_document == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
"clearance_date",
d.clearance_date,
)
if invalid_document or invalid_cheque_date:
msg = _("<p>Please correct the following row(s):</p><ul>")
if invalid_document:
msg += _("<li>Payment document required for row(s): {0}</li>").format(
", ".join(invalid_document)
)
else:
# using db_set to trigger notification
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
payment_entry.db_set("clearance_date", d.clearance_date)
if invalid_cheque_date:
msg += _("<li>Clearance date must be after cheque date for row(s): {0}</li>").format(
", ".join(invalid_cheque_date)
)
clearance_date_updated = True
msg += "</ul>"
frappe.throw(_(msg))
return
if not entries_to_update:
if clearance_date_updated:
self.get_payment_entries()
msgprint(_("Clearance Date updated"))
else:
msgprint(_("Clearance Date not mentioned"))
return
for d in entries_to_update:
if d.payment_document == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
"clearance_date",
d.clearance_date,
)
else:
# using db_set to trigger notification
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
payment_entry.db_set("clearance_date", d.clearance_date)
self.get_payment_entries()
msgprint(_("Clearance Date updated"))
def get_payment_entries_for_bank_clearance(
@@ -155,10 +137,8 @@ def get_payment_entries_for_bank_clearance(
entries = []
condition = ""
pe_condition = ""
if not include_reconciled_entries:
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 = frappe.db.sql(
f"""
@@ -183,20 +163,19 @@ def get_payment_entries_for_bank_clearance(
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
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
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}
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
order by
pe.posting_date ASC, pe.name DESC
posting_date ASC, name DESC
""",
{
"account": account,

View File

@@ -146,7 +146,6 @@
"fieldname": "iban",
"fieldtype": "Data",
"label": "IBAN",
"options": "IBAN",
"read_only": 1
},
{
@@ -215,10 +214,9 @@
"read_only": 1
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-08-29 11:52:33.550847",
"modified": "2024-03-27 13:06:37.731207",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Guarantee",
@@ -252,10 +250,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "customer",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "customer"
}
}

View File

@@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import cint, create_batch, flt
from frappe.utils import cint, flt
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
@@ -377,17 +377,16 @@ def auto_reconcile_vouchers(
bank_transactions = get_bank_transactions(bank_account)
if len(bank_transactions) > 10:
for bank_transaction_batch in create_batch(bank_transactions, 1000):
frappe.enqueue(
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
queue="long",
bank_transactions=bank_transaction_batch,
from_date=from_date,
to_date=to_date,
filter_by_reference_date=filter_by_reference_date,
from_reference_date=from_reference_date,
to_reference_date=to_reference_date,
)
frappe.enqueue(
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
queue="long",
bank_transactions=bank_transactions,
from_date=from_date,
to_date=to_date,
filter_by_reference_date=filter_by_reference_date,
from_reference_date=from_reference_date,
to_reference_date=to_reference_date,
)
frappe.msgprint(_("Auto Reconciliation has started in the background"))
else:
start_auto_reconcile(
@@ -409,7 +408,7 @@ def start_auto_reconcile(
for transaction in bank_transactions:
linked_payments = get_linked_payments(
transaction.name,
["payment_entry", "journal_entry", "sales_invoice"],
["payment_entry", "journal_entry"],
from_date,
to_date,
filter_by_reference_date,
@@ -666,7 +665,7 @@ def get_matching_queries(
queries.append(query)
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
query = get_si_matching_query(exact_match, currency, common_filters, transaction)
query = get_si_matching_query(exact_match, currency, common_filters)
queries.append(query)
if transaction.withdrawal > 0.0:
@@ -854,14 +853,11 @@ def get_je_matching_query(
return query
def get_si_matching_query(exact_match, currency, common_filters, transaction):
def get_si_matching_query(exact_match, currency, common_filters):
# get matching sales invoice query
si = frappe.qb.DocType("Sales Invoice")
sip = frappe.qb.DocType("Sales Invoice Payment")
ref_condition = sip.reference_no == transaction.reference_number
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
amount_equality = sip.amount == common_filters.amount
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
amount_condition = amount_equality if exact_match else sip.amount > 0.0
@@ -874,11 +870,11 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
.join(si)
.on(sip.parent == si.name)
.select(
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
(party_rank + amount_rank + 1).as_("rank"),
ConstantColumn("Sales Invoice").as_("doctype"),
si.name,
sip.amount.as_("paid_amount"),
sip.reference_no,
ConstantColumn("").as_("reference_no"),
ConstantColumn("").as_("reference_date"),
si.customer.as_("party"),
ConstantColumn("Customer").as_("party_type"),
@@ -892,9 +888,6 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
.where(si.currency == currency)
)
if frappe.flags.auto_reconcile_vouchers is True:
query = query.where(ref_condition)
return query

View File

@@ -252,7 +252,7 @@ frappe.ui.form.on("Bank Statement Import", {
open_url_post(method, {
doctype: "Bank Transaction",
export_records: "blank_template",
export_records: "5_records",
export_fields: {
"Bank Transaction": [
"date",

View File

@@ -111,54 +111,20 @@ class BankStatementImport(DataImport):
return None
def preprocess_mt940_content(content: str) -> str:
"""Preprocess MT940 content to fix statement number format issues.
The MT940 standard expects statement numbers to be maximum 5 digits,
but some banks provide longer statement numbers that cause parsing errors.
This function truncates statement numbers longer than 5 digits to the last 5 digits.
"""
# Fast-path: bail if no :28C: tag exists
if ":28C:" not in content:
return content
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
pattern = re.compile(r"(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$")
def replace_statement_number(match):
prefix = match.group(1) # ':28C:'
statement_num = match.group(2) # The statement number
sequence_part = match.group(3) or "" # The sequence part like '/1'
trailing_space = match.group(4) or "" # Preserve trailing whitespace
# If statement number is longer than 5 digits, truncate to last 5 digits
if len(statement_num) > 5:
statement_num = statement_num[-5:]
return prefix + statement_num + sequence_part + trailing_space
# Apply the replacement
processed_content = pattern.sub(replace_statement_number, content)
return processed_content
@frappe.whitelist()
def convert_mt940_to_csv(data_import, mt940_file_path):
doc = frappe.get_doc("Bank Statement Import", data_import)
_file_doc, content = get_file(mt940_file_path)
file_doc, content = get_file(mt940_file_path)
is_mt940 = is_mt940_format(content)
if not is_mt940:
if not is_mt940_format(content):
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
if is_mt940 and not doc.import_mt940_fromat:
if is_mt940_format(content) and not doc.import_mt940_fromat:
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
try:
# Preprocess MT940 content to fix statement number format issues
processed_content = preprocess_mt940_content(content)
transactions = mt940.parse(processed_content)
transactions = mt940.parse(content)
except Exception as e:
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
@@ -283,7 +249,6 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
def update_mapping_db(bank, template_options):
"""Update bank transaction mapping database with template options."""
bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping:
d.delete()
@@ -295,7 +260,6 @@ def update_mapping_db(bank, template_options):
def add_bank_account(data, bank_account):
"""Add bank account information to data rows."""
bank_account_loc = None
if "Bank Account" not in data[0]:
data[0].append("Bank Account")
@@ -312,7 +276,6 @@ def add_bank_account(data, bank_account):
def write_files(import_file, data):
"""Write processed data to CSV or Excel files."""
full_file_path = import_file.file_doc.get_full_path()
parts = import_file.file_doc.get_extension()
extension = parts[1]
@@ -322,12 +285,11 @@ def write_files(import_file, data):
with open(full_file_path, "w", newline="") as file:
writer = csv.writer(file)
writer.writerows(data)
elif extension in ("xlsx", "xls"):
elif extension == "xlsx" or "xls":
write_xlsx(data, "trans", file_path=full_file_path)
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
"""Write data to Excel file with formatting."""
# from xlsx utils with changes
column_widths = column_widths or []
if wb is None:

View File

@@ -1,209 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
is_mt940_format,
preprocess_mt940_content,
)
from frappe.tests import IntegrationTestCase
class TestBankStatementImport(unittest.TestCase):
"""Unit tests for Bank Statement Import functions"""
def test_preprocess_mt940_content_with_long_statement_number(self):
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
# Test case with 6-digit statement number (167619 -> 67619)
mt940_content = ":28C:167619/1"
expected_content = ":28C:67619/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_with_normal_statement_number(self):
"""Test that statement numbers with 5 or fewer digits are unchanged"""
# Test case with 5-digit statement number (should remain unchanged)
mt940_content = ":28C:12345/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should be unchanged
# Test case with 4-digit statement number (should remain unchanged)
mt940_content = ":28C:1234/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should be unchanged
def test_preprocess_mt940_content_without_sequence_number(self):
"""Test statement number truncation without sequence number"""
# Test case with long statement number but no sequence (no /1)
mt940_content = ":28C:987654321"
expected_content = ":28C:54321"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_multiple_occurrences(self):
"""Test multiple statement numbers in the same content"""
mt940_content = """:28C:167619/1
:28C:987654/2"""
expected_content = """:28C:67619/1
:28C:87654/2"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_edge_cases(self):
"""Test edge cases like empty content and content without :28C: tags"""
# Test empty content
self.assertEqual(preprocess_mt940_content(""), "")
# Test content without :28C: tags
content_without_28c = """:20:STARTUMSE
:25:12345678901234567890
:60F:C031002EUR0,00"""
result = preprocess_mt940_content(content_without_28c)
self.assertEqual(result, content_without_28c) # Should be unchanged
def test_preprocess_mt940_content_with_full_mt940_document(self):
"""Test preprocessing with complete MT940 document"""
mt940_content = """:20:STARTUMSE
:25:12345678901234567890
:28C:167619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
:62F:C031002EUR-123,45
-"""
expected_content = """:20:STARTUMSE
:25:12345678901234567890
:28C:67619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
:62F:C031002EUR-123,45
-"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_is_mt940_format_detection(self):
"""Test MT940 format detection function"""
# Valid MT940 content with all required tags
valid_mt940 = """:20:STARTUMSE
:25:12345678901234567890
:28C:167619/1
:60F:C031002EUR0,00
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
self.assertTrue(is_mt940_format(valid_mt940))
# Invalid MT940 content (CSV format)
invalid_mt940 = """Date,Description,Amount
2023-01-01,Test Transaction,100.00
2023-01-02,Another Transaction,-50.00"""
self.assertFalse(is_mt940_format(invalid_mt940))
# Partially valid MT940 (missing some required tags)
partial_mt940 = """:20:STARTUMSE
:25:12345678901234567890
:60F:C031002EUR0,00"""
self.assertFalse(is_mt940_format(partial_mt940))
# Empty content
self.assertFalse(is_mt940_format(""))
def test_preprocess_mt940_content_boundary_conditions(self):
"""Test boundary conditions for statement number length"""
# Test exactly 6 digits (should be truncated)
mt940_content = ":28C:123456/1"
expected_content = ":28C:23456/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test exactly 5 digits (should remain unchanged)
mt940_content = ":28C:12345/1"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content)
# Test very long statement number
mt940_content = ":28C:123456789012345/1"
expected_content = ":28C:12345/1" # Last 5 digits
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
def test_preprocess_mt940_content_real_world_case(self):
"""Test with real-world MT940 content that was failing in production"""
# This is based on actual MT940 content that was causing parsing errors (sanitized)
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF167619
:25:1234567890
:28C:167619/1
:60F:C250622USD0,00
:61:2507170717C100000,00NMSCNOREF
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
:61:2507310731D305,62NMSCTBMS-1234567890
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2508030803D1066,00NMSC123456789
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
:61:2508060806D2000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2508140814D5000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2509190919D900,00NMSCUPI-123456789
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
:61:2509190919D2606,00NMSCUPI-123456789
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
:62F:C250922USD88123,38
-}"""
# Expected result with statement number 167619 truncated to 67619
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF167619
:25:1234567890
:28C:67619/1
:60F:C250622USD0,00
:61:2507170717C100000,00NMSCNOREF
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
:61:2507310731D305,62NMSCTBMS-1234567890
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2508030803D1066,00NMSC123456789
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
:61:2508060806D2000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2508140814D5000,00NMSCUPI-123456789
:86:UPI/TEST USER/123456789/PaidViaTestApp
:61:2509190919D900,00NMSCUPI-123456789
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
:61:2509190919D2606,00NMSCUPI-123456789
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
:62F:C250922USD88123,38
-}"""
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Verify that the problematic statement number was actually changed
self.assertIn(":28C:67619/1", result)
self.assertNotIn(":28C:167619/1", result)
# Verify that other content remains unchanged
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
def test_preprocess_mt940_content_whitespace_variants(self):
"""Test handling of whitespace and different line endings"""
# Test with trailing spaces
mt940_content = ":28C:167619/1 \n"
expected_content = ":28C:67619/1 \n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test with Windows line endings (CRLF)
mt940_content = ":28C:167619/1\r\n"
expected_content = ":28C:67619/1\r\n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, expected_content)
# Test with leading spaces (should not match as it's not line start)
mt940_content = " :28C:167619/1\n"
result = preprocess_mt940_content(mt940_content)
self.assertEqual(result, mt940_content) # Should remain unchanged
class TestBankStatementImport(IntegrationTestCase):
pass

View File

@@ -116,7 +116,7 @@
{
"allow_on_submit": 1,
"fieldname": "reference_number",
"fieldtype": "Small Text",
"fieldtype": "Data",
"label": "Reference Number"
},
{
@@ -223,8 +223,7 @@
{
"fieldname": "bank_party_iban",
"fieldtype": "Data",
"label": "Party IBAN (Bank Statement)",
"options": "IBAN"
"label": "Party IBAN (Bank Statement)"
},
{
"fieldname": "bank_party_account_number",
@@ -239,7 +238,7 @@
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-09-26 17:06:29.207673",
"modified": "2025-06-18 17:24:57.044666",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -36,7 +36,7 @@ class BankTransaction(Document):
party: DF.DynamicLink | None
party_type: DF.Link | None
payment_entries: DF.Table[BankTransactionPayments]
reference_number: DF.SmallText | None
reference_number: DF.Data | None
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
transaction_id: DF.Data | None
transaction_type: DF.Data | None

View File

@@ -7,9 +7,6 @@ from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
IBAN_1 = "DE02000000003716541159"
IBAN_2 = "DE02500105170137075030"
class TestAutoMatchParty(IntegrationTestCase):
@classmethod
@@ -25,24 +22,24 @@ class TestAutoMatchParty(IntegrationTestCase):
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
def test_match_by_account_number(self):
create_supplier_for_match(account_no=IBAN_1[11:])
create_supplier_for_match(account_no="000000003716541159")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
account_no=IBAN_1[11:],
iban=IBAN_1,
account_no="000000003716541159",
iban="DE02000000003716541159",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "John Doe & Co.")
def test_match_by_iban(self):
create_supplier_for_match(iban=IBAN_1)
create_supplier_for_match(iban="DE02000000003716541159")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="c5455a224602afaa51592a9d9250600d",
account_no=IBAN_1[11:],
iban=IBAN_1,
account_no="000000003716541159",
iban="DE02000000003716541159",
)
self.assertEqual(doc.party_type, "Supplier")
@@ -54,7 +51,7 @@ class TestAutoMatchParty(IntegrationTestCase):
withdrawal=1200,
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
party_name="Ella Jackson",
iban=IBAN_2,
iban="DE04000000003716545346",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "Jackson Ella W.")

View File

@@ -23,8 +23,8 @@ frappe.ui.form.on("Budget", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
if (value) {
frappe.db.get_single_value("Accounts Settings", "use_new_budget_controller").then((value) => {
if (!value) {
frm.get_field("control_action_for_cumulative_expense_section").hide();
}
});

View File

@@ -145,10 +145,8 @@ def validate_expense_against_budget(args, expense_amount=0):
if not frappe.db.count("Budget", cache=True):
return
if not args.fiscal_year:
if args.get("company") and not args.fiscal_year:
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
if args.get("company"):
frappe.flags.exception_approver_role = frappe.get_cached_value(
"Company", args.get("company"), "exception_budget_approver_role"
)
@@ -304,7 +302,7 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
def get_expense_breakup(args, currency, budget_against):
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
msg = "<hr>Total Expenses booked through - <ul>"
common_filters = frappe._dict(
{
@@ -318,7 +316,7 @@ def get_expense_breakup(args, currency, budget_against):
"<li>"
+ frappe.utils.get_link_to_report(
"General Ledger",
label=_("Actual Expenses"),
label="Actual Expenses",
filters=common_filters.copy().update(
{
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
@@ -336,7 +334,7 @@ def get_expense_breakup(args, currency, budget_against):
"<li>"
+ frappe.utils.get_link_to_report(
"Material Request",
label=_("Material Requests"),
label="Material Requests",
report_type="Report Builder",
doctype="Material Request",
filters=common_filters.copy().update(
@@ -359,7 +357,7 @@ def get_expense_breakup(args, currency, budget_against):
"<li>"
+ frappe.utils.get_link_to_report(
"Purchase Order",
label=_("Unbilled Orders"),
label="Unbilled Orders",
report_type="Report Builder",
doctype="Purchase Order",
filters=common_filters.copy().update(

View File

@@ -24,7 +24,7 @@ class TestBudget(ERPNextTestSuite):
cls.make_projects()
def setUp(self):
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
frappe.db.set_single_value("Accounts Settings", "use_new_budget_controller", True)
def test_monthly_budget_crossed_ignore(self):
set_total_expense_zero(nowdate(), "cost_center")
@@ -113,10 +113,6 @@ class TestBudget(ERPNextTestSuite):
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
)
mr = frappe.get_doc(
{
"doctype": "Material Request",
@@ -130,7 +126,7 @@ class TestBudget(ERPNextTestSuite):
"uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC",
"schedule_date": nowdate(),
"rate": accumulated_limit + 1,
"rate": 100000,
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
}

View File

@@ -462,8 +462,9 @@ def unset_existing_data(company):
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
dt = frappe.qb.DocType(doctype)
frappe.qb.from_(dt).where(dt.company == company).delete().run()
frappe.db.sql(
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
)
def set_default_accounts(company):

View File

@@ -3,7 +3,6 @@
import unittest
import frappe
from frappe.query_builder.functions import Sum
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, today
@@ -191,31 +190,6 @@ class TestCostCenterAllocation(IntegrationTestCase):
coa2.cancel()
jv.cancel()
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
cca = create_cost_center_allocation(
"_Test Company",
"Main Cost Center 1 - _TC",
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
)
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
gl_entry = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gl_entry)
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
.where(gl_entry.voucher_type == "Sales Invoice")
.where(gl_entry.voucher_no == si.name)
).run(as_dict=1)
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
si.cancel()
cca.cancel()
def create_cost_center_allocation(
company,

View File

@@ -11,7 +11,6 @@
-> Resolves dunning automatically
"""
import json
import frappe
@@ -164,66 +163,43 @@ class Dunning(AccountsController):
]
def update_linked_dunnings(doc, previous_outstanding_amount):
if (
doc.doctype != "Sales Invoice"
or doc.is_return
or previous_outstanding_amount == doc.outstanding_amount
):
return
def resolve_dunning(doc, state):
"""
Check if all payments have been made and resolve dunning, if yes. Called
when a Payment Entry is submitted.
"""
for reference in doc.references:
# Consider partial and full payments:
# Submitting full payment: outstanding_amount will be 0
# Submitting 1st partial payment: outstanding_amount will be the pending installment
# Cancelling full payment: outstanding_amount will revert to total amount
# Cancelling last partial payment: outstanding_amount will revert to pending amount
submit_condition = reference.outstanding_amount < reference.total_amount
cancel_condition = reference.outstanding_amount <= reference.total_amount
to_resolve = doc.outstanding_amount < previous_outstanding_amount
state = "Unresolved" if to_resolve else "Resolved"
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
if not dunnings:
return
if reference.reference_doctype == "Sales Invoice" and (
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
invoices = set()
payment_schedule_ids = set()
for dunning in dunnings:
resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
for dunning in dunnings:
for overdue_payment in dunning.overdue_payments:
invoices.add(overdue_payment.sales_invoice)
if overdue_payment.payment_schedule:
payment_schedule_ids.add(overdue_payment.payment_schedule)
new_status = "Resolved" if resolve else "Unresolved"
invoice_outstanding_amounts = dict(
frappe.get_all(
"Sales Invoice",
filters={"name": ["in", list(invoices)]},
fields=["name", "outstanding_amount"],
as_list=True,
)
)
ps_outstanding_amounts = (
dict(
frappe.get_all(
"Payment Schedule",
filters={"name": ["in", list(payment_schedule_ids)]},
fields=["name", "outstanding"],
as_list=True,
)
)
if payment_schedule_ids
else {}
)
for dunning in dunnings:
has_outstanding = False
for overdue_payment in dunning.overdue_payments:
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
if has_outstanding:
break
new_status = "Resolved" if not has_outstanding else "Unresolved"
if dunning.status != new_status:
dunning.status = new_status
dunning.save()
if dunning.status != new_status:
dunning.status = new_status
dunning.save()
def get_linked_dunnings_as_per_state(sales_invoice, state):

View File

@@ -139,64 +139,6 @@ class TestDunning(IntegrationTestCase):
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_resolution_from_credit_note(self):
"""
Test that dunning is resolved when a credit note is issued against the original invoice.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 0
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
credit_note.cancel()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def test_dunning_not_affected_by_standalone_credit_note(self):
"""
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
"""
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -10), qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning.submit()
self.assertEqual(dunning.status, "Unresolved")
credit_note = frappe.copy_doc(sales_invoice)
credit_note.is_return = 1
credit_note.return_against = sales_invoice.name
credit_note.update_outstanding_for_self = 1
for item in credit_note.items:
item.qty = -item.qty
credit_note.save()
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
credit_note.submit()
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -1 * overdue_days)

View File

@@ -134,8 +134,7 @@ class ExchangeRateRevaluation(Document):
accounts = self.get_accounts_data()
if accounts:
for acc in accounts:
if acc.get("gain_loss"):
self.append("accounts", acc)
self.append("accounts", acc)
@frappe.whitelist()
def get_accounts_data(self):

View File

@@ -29,17 +29,14 @@
"against_voucher",
"voucher_detail_no",
"transaction_exchange_rate",
"reporting_currency_exchange_rate",
"amounts_section",
"debit_in_account_currency",
"debit",
"debit_in_transaction_currency",
"debit_in_reporting_currency",
"column_break_bm1w",
"credit_in_account_currency",
"credit",
"credit_in_transaction_currency",
"credit_in_reporting_currency",
"dimensions_section",
"cost_center",
"column_break_lmnm",
@@ -356,31 +353,13 @@
{
"fieldname": "column_break_8abq",
"fieldtype": "Column Break"
},
{
"fieldname": "debit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "credit_in_reporting_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Reporting Currency",
"options": "Company:company:reporting_currency"
},
{
"fieldname": "reporting_currency_exchange_rate",
"fieldtype": "Float",
"label": "Reporting Currency Exchange Rate",
"precision": "9"
}
],
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2025-08-22 12:57:17.750252",
"modified": "2025-03-21 15:29:11.221890",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
@@ -411,9 +390,8 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -18,9 +18,8 @@ from erpnext.accounts.party import (
validate_party_frozen_disabled,
validate_party_gle_currency,
)
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, ReportingCurrencyExchangeNotFoundError
from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency
exclude_from_linked_with = True
@@ -43,11 +42,9 @@ class GLEntry(Document):
cost_center: DF.Link | None
credit: DF.Currency
credit_in_account_currency: DF.Currency
credit_in_reporting_currency: DF.Currency
credit_in_transaction_currency: DF.Currency
debit: DF.Currency
debit_in_account_currency: DF.Currency
debit_in_reporting_currency: DF.Currency
debit_in_transaction_currency: DF.Currency
due_date: DF.Date | None
finance_book: DF.Link | None
@@ -60,7 +57,6 @@ class GLEntry(Document):
posting_date: DF.Date | None
project: DF.Link | None
remarks: DF.Text | None
reporting_currency_exchange_rate: DF.Float
to_rename: DF.Check
transaction_currency: DF.Link | None
transaction_date: DF.Date | None
@@ -92,8 +88,6 @@ class GLEntry(Document):
self.validate_party()
self.validate_currency()
self.set_amount_in_reporting_currency()
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
@@ -137,20 +131,18 @@ class GLEntry(Document):
if not self.is_cancelled and not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not frappe.flags.party_not_required: # skipping validation if party is not required
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
if account_type == "Receivable":
frappe.throw(
_("{0} {1}: Customer is required against Receivable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
elif account_type == "Payable":
frappe.throw(
_("{0} {1}: Supplier is required against Payable account {2}").format(
self.voucher_type, self.voucher_no, self.account
)
)
# Zero value transaction is not allowed
if not (
@@ -232,23 +224,26 @@ class GLEntry(Document):
def validate_account_details(self, adv_adj):
"""Account must be ledger, active and not freezed"""
account = frappe.get_cached_value(
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
)
ret = frappe.db.sql(
"""select is_group, docstatus, company
from tabAccount where name=%s""",
self.account,
as_dict=1,
)[0]
if account.is_group == 1:
if ret.is_group == 1:
frappe.throw(
_(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, self.account)
)
if account.docstatus == 2:
if ret.docstatus == 2:
frappe.throw(
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
)
if account.company != self.company:
if ret.company != self.company:
frappe.throw(
_("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company
@@ -256,7 +251,7 @@ class GLEntry(Document):
)
def validate_cost_center(self):
if not self.cost_center or self.is_cancelled:
if not self.cost_center:
return
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
@@ -300,25 +295,6 @@ class GLEntry(Document):
if self.party_type and self.party:
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
def set_amount_in_reporting_currency(self):
default_currency, reporting_currency = frappe.get_cached_value(
"Company", self.company, ["default_currency", "reporting_currency"]
)
transaction_date = self.transaction_date or self.posting_date
self.reporting_currency_exchange_rate = get_exchange_rate(
default_currency, reporting_currency, transaction_date
)
if not self.reporting_currency_exchange_rate:
frappe.throw(
title=_("Reporting Currency Exchange Not Found"),
msg=_(
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
).format(default_currency, reporting_currency, transaction_date),
exc=ReportingCurrencyExchangeNotFoundError,
)
self.debit_in_reporting_currency = flt(self.debit * self.reporting_currency_exchange_rate)
self.credit_in_reporting_currency = flt(self.credit * self.reporting_currency_exchange_rate)
def validate_and_set_fiscal_year(self):
if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
@@ -335,7 +311,7 @@ def validate_balance_type(account, adv_adj=False):
if balance_must_be:
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
from `tabGL Entry` where account = %s""",
account,
)[0][0]
@@ -409,7 +385,7 @@ def update_outstanding_amt(
)
)
if against_voucher_type in OUTSTANDING_DOCTYPES:
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
# Didn't use db_set for optimization purpose
@@ -486,9 +462,4 @@ def rename_temporarily_named_docs(doctype):
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
(newname, now(), oldname),
)
for hook_type in ("on_gle_rename", "on_sle_rename"):
for hook in frappe.get_hooks(hook_type):
frappe.call(hook, newname=newname, oldname=oldname)
frappe.db.commit()

View File

@@ -196,7 +196,6 @@ frappe.ui.form.on("Journal Entry", {
});
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
voucher_type: function (frm) {

View File

@@ -46,6 +46,7 @@
"reference",
"clearance_date",
"remark",
"paid_loan",
"inter_company_journal_entry_reference",
"column_break98",
"bill_no",
@@ -64,7 +65,6 @@
"addtional_info",
"mode_of_payment",
"payment_order",
"party_not_required",
"column_break3",
"is_opening",
"stock_entry",
@@ -310,6 +310,13 @@
"oldfieldtype": "Small Text",
"read_only": 1
},
{
"fieldname": "paid_loan",
"fieldtype": "Data",
"hidden": 1,
"label": "Paid Loan",
"print_hide": 1
},
{
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
"fieldname": "inter_company_journal_entry_reference",
@@ -578,14 +585,6 @@
"fieldname": "get_balance_for_periodic_accounting",
"fieldtype": "Button",
"label": "Get Balance"
},
{
"default": "0",
"fieldname": "party_not_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Party Not Required",
"no_copy": 1
}
],
"icon": "fa fa-file-text",
@@ -600,7 +599,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-09-29 13:05:46.982277",
"modified": "2025-06-17 15:18:13.322681",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -24,7 +24,6 @@ from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_advance_payment_doctypes,
get_balance_on,
get_stock_accounts,
get_stock_and_account_balance,
@@ -72,7 +71,7 @@ class JournalEntry(AccountsController):
mode_of_payment: DF.Link | None
multi_currency: DF.Check
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
party_not_required: DF.Check
paid_loan: DF.Data | None
pay_to_recd_from: DF.Data | None
payment_order: DF.Link | None
periodic_entry_difference_account: DF.Link | None
@@ -152,8 +151,8 @@ class JournalEntry(AccountsController):
if self.docstatus == 0:
self.apply_tax_withholding()
if self.is_new() or not self.title:
self.title = self.get_title()
self.title = self.get_title()
def validate_advance_accounts(self):
journal_accounts = set([x.account for x in self.accounts])
@@ -194,8 +193,10 @@ class JournalEntry(AccountsController):
def on_submit(self):
self.validate_cheque_info()
self.make_gl_entries()
self.check_credit_limit()
self.make_gl_entries()
self.make_advance_payment_ledger_entries()
self.update_advance_paid()
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
@@ -297,6 +298,8 @@ class JournalEntry(AccountsController):
"Advance Payment Ledger Entry",
)
self.make_gl_entries(1)
self.make_advance_payment_ledger_entries()
self.update_advance_paid()
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
self.unlink_inter_company_jv()
@@ -306,6 +309,20 @@ class JournalEntry(AccountsController):
def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account
def update_advance_paid(self):
advance_paid = frappe._dict()
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in advance_payment_doctypes:
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items():
for voucher_no in list(set(order_list)):
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
def validate_inter_company_accounts(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
doc = frappe.db.get_value(
@@ -645,11 +662,8 @@ class JournalEntry(AccountsController):
def validate_party(self):
for d in self.get("accounts"):
account_type = frappe.get_cached_value("Account", d.account, "account_type")
if account_type in ["Receivable", "Payable"]:
if (
not (d.party_type and d.party) and not self.party_not_required
): # skipping validation if party_not_required is passed via payroll entry
if not (d.party_type and d.party):
frappe.throw(
_(
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
@@ -658,8 +672,6 @@ class JournalEntry(AccountsController):
elif (
d.party_type
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
and d.party_type
!= "Employee" # making an excpetion for employee since they can be both payable and receivable
):
frappe.throw(
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
@@ -1185,70 +1197,49 @@ class JournalEntry(AccountsController):
self.transaction_exchange_rate = row.exchange_rate
break
advance_doctypes = get_advance_payment_doctypes()
for d in self.get("accounts"):
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark]
r = [x for x in r if x]
remarks = "\n".join(r)
row = {
"account": d.account,
"party_type": d.party_type,
"due_date": self.due_date,
"party": d.party,
"against": d.against_account,
"debit": flt(d.debit, d.precision("debit")),
"credit": flt(d.credit, d.precision("credit")),
"account_currency": d.account_currency,
"debit_in_account_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
),
"credit_in_account_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
),
"transaction_currency": self.transaction_currency,
"transaction_exchange_rate": self.transaction_exchange_rate,
"debit_in_transaction_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
"credit_in_transaction_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
"against_voucher_type": d.reference_type,
"against_voucher": d.reference_name,
"remarks": remarks,
"voucher_detail_no": d.reference_detail_no,
"cost_center": d.cost_center,
"project": d.project,
"finance_book": self.finance_book,
"advance_voucher_type": d.advance_voucher_type,
"advance_voucher_no": d.advance_voucher_no,
}
if d.reference_type in advance_doctypes:
row.update(
{
"against_voucher_type": self.doctype,
"against_voucher": self.name,
"advance_voucher_type": d.reference_type,
"advance_voucher_no": d.reference_name,
}
)
# set flag to skip party validation
account_type = frappe.get_cached_value("Account", d.account, "account_type")
if account_type in ["Receivable", "Payable"] and self.party_not_required:
frappe.flags.party_not_required = True
gl_map.append(
self.get_gl_dict(
row,
{
"account": d.account,
"party_type": d.party_type,
"due_date": self.due_date,
"party": d.party,
"against": d.against_account,
"debit": flt(d.debit, d.precision("debit")),
"credit": flt(d.credit, d.precision("credit")),
"account_currency": d.account_currency,
"debit_in_account_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
),
"credit_in_account_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
),
"transaction_currency": self.transaction_currency,
"transaction_exchange_rate": self.transaction_exchange_rate,
"debit_in_transaction_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
"credit_in_transaction_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
"against_voucher_type": d.reference_type,
"against_voucher": d.reference_name,
"remarks": remarks,
"voucher_detail_no": d.reference_detail_no,
"cost_center": d.cost_center,
"project": d.project,
"finance_book": self.finance_book,
},
item=d,
)
)
@@ -1273,7 +1264,6 @@ class JournalEntry(AccountsController):
merge_entries=merge_entries,
update_outstanding=update_outstanding,
)
frappe.flags.party_not_required = False
if cancel:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
@@ -1806,14 +1796,6 @@ def make_inter_company_journal_entry(name, voucher_type, company):
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
if existing_reverse:
frappe.throw(
_("A Reverse Journal Entry {0} already exists for this Journal Entry.").format(
get_link_to_form("Journal Entry", existing_reverse)
)
)
from frappe.model.mapper import get_mapped_doc
def post_process(source, target):

View File

@@ -8,7 +8,6 @@ from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
from erpnext.exceptions import InvalidAccountCurrency
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
class TestJournalEntry(IntegrationTestCase):
@@ -580,27 +579,6 @@ class TestJournalEntry(IntegrationTestCase):
]
self.assertEqual(expected, actual)
def test_pay_to_recd_from(self):
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
jv.pay_to_recd_from = "_Test Receiver"
jv.save()
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
jv.pay_to_recd_from = "_Test Receiver 2"
jv.save()
jv.submit()
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
def test_credit_limit_for_customer(self):
customer = make_customer("_Test New Customer")
set_credit_limit("_Test New Customer", "_Test Company", 50)
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = customer
jv.save()
self.assertRaises(frappe.ValidationError, jv.submit)
def make_journal_entry(
account1,

View File

@@ -32,8 +32,6 @@
"reference_name",
"reference_due_date",
"reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"col_break3",
"is_advance",
"user_remark",
@@ -264,37 +262,20 @@
"hidden": 1,
"label": "Reference Detail No",
"no_copy": 1
},
{
"fieldname": "advance_voucher_type",
"fieldtype": "Link",
"label": "Advance Voucher Type",
"no_copy": 1,
"options": "DocType",
"read_only": 1
},
{
"fieldname": "advance_voucher_no",
"fieldtype": "Dynamic Link",
"label": "Advance Voucher No",
"no_copy": 1,
"options": "advance_voucher_type",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-09-29 13:01:48.916517",
"modified": "2024-03-27 13:09:58.647732",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,9 +17,8 @@ class JournalEntryAccount(Document):
account: DF.Link
account_currency: DF.Link | None
account_type: DF.Data | None
advance_voucher_no: DF.DynamicLink | None
advance_voucher_type: DF.Link | None
against_account: DF.Text | None
balance: DF.Currency
bank_account: DF.Link | None
cost_center: DF.Link | None
credit: DF.Currency
@@ -32,6 +31,7 @@ class JournalEntryAccount(Document):
parentfield: DF.Data
parenttype: DF.Data
party: DF.DynamicLink | None
party_balance: DF.Currency
party_type: DF.Link | None
project: DF.Link | None
reference_detail_no: DF.Data | None

View File

@@ -25,7 +25,6 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"help_section",
"loyalty_program_help"
],
@@ -145,12 +144,6 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"links": [],

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, today
@@ -56,29 +55,21 @@ def get_loyalty_details(
if not expiry_date:
expiry_date = today()
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
)
.groupby(LoyaltyPointEntry.customer)
)
condition = ""
if company:
query = query.where(LoyaltyPointEntry.company == company)
condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry:
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
condition += " and expiry_date>='%s' " % expiry_date
loyalty_point_details = query.run(as_dict=True)
loyalty_point_details = frappe.db.sql(
f"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
group by customer""",
(customer, loyalty_program, expiry_date),
as_dict=1,
)
if loyalty_point_details:
return loyalty_point_details[0]

View File

@@ -14,7 +14,6 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"section_break_4",
"invoices"
],
@@ -64,12 +63,6 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",

View File

@@ -74,6 +74,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party
party_link.save()
party_link.save(ignore_permissions=True)
return party_link

View File

@@ -273,7 +273,6 @@ frappe.ui.form.on("Payment Entry", {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
contact_person: function (frm) {
@@ -585,7 +584,6 @@ frappe.ui.form.on("Payment Entry", {
if (frm.doc.payment_type == "Pay") {
frm.events.paid_amount(frm);
}
frm.events.paid_from_account_currency(frm);
}
);
},
@@ -608,7 +606,6 @@ frappe.ui.form.on("Payment Entry", {
frm.events.received_amount(frm);
}
}
frm.events.paid_to_account_currency(frm);
}
);
},

View File

@@ -46,9 +46,7 @@ from erpnext.accounts.party import (
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_advance_payment_doctypes,
get_outstanding_invoices,
get_reconciliation_effect_date,
)
from erpnext.controllers.accounts_controller import (
AccountsController,
@@ -199,10 +197,12 @@ class PaymentEntry(AccountsController):
def on_submit(self):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.update_payment_requests()
self.update_payment_schedule()
self.make_gl_entries()
self.update_outstanding_amounts()
self.update_payment_schedule()
self.update_payment_requests()
self.make_advance_payment_ledger_entries()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def validate_for_repost(self):
@@ -302,11 +302,13 @@ class PaymentEntry(AccountsController):
"Advance Payment Ledger Entry",
)
super().on_cancel()
self.update_payment_requests(cancel=True)
self.update_payment_schedule(cancel=1)
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.update_payment_requests(cancel=True)
self.make_advance_payment_ledger_entries()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def update_payment_requests(self, cancel=False):
@@ -637,7 +639,7 @@ class PaymentEntry(AccountsController):
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
if not self.get(field):
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
def validate_reference_documents(self):
valid_reference_doctypes = self.get_valid_reference_doctypes()
@@ -1097,7 +1099,10 @@ class PaymentEntry(AccountsController):
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
if d.reference_doctype in get_advance_payment_doctypes():
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if d.reference_doctype in advance_payment_doctypes:
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
@@ -1379,7 +1384,10 @@ class PaymentEntry(AccountsController):
if not self.party_account:
return
advance_payment_doctypes = get_advance_payment_doctypes()
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.payment_type == "Receive":
against_account = self.paid_to
else:
@@ -1435,27 +1443,23 @@ class PaymentEntry(AccountsController):
dr_or_cr + "_in_transaction_currency": d.allocated_amount
if self.transaction_currency == self.party_account_currency
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
"advance_voucher_type": d.advance_voucher_type,
"advance_voucher_no": d.advance_voucher_no,
},
item=self,
)
)
if d.reference_doctype in advance_payment_doctypes:
# advance reference
gle.update(
{
"against_voucher_type": self.doctype,
"against_voucher": self.name,
"advance_voucher_type": d.reference_doctype,
"advance_voucher_no": d.reference_name,
}
)
elif self.book_advance_payments_in_separate_party_account:
# Do not reference Invoices while Advance is in separate party account
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
if self.book_advance_payments_in_separate_party_account:
if d.reference_doctype in advance_payment_doctypes:
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
gle.update(
{
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
}
)
else:
# Do not reference Invoices while Advance is in separate party account
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
else:
gle.update(
{
@@ -1560,14 +1564,29 @@ class PaymentEntry(AccountsController):
"voucher_no": self.name,
"voucher_detail_no": invoice.name,
}
if invoice.reconcile_effect_on:
posting_date = invoice.reconcile_effect_on
else:
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
posting_date = get_reconciliation_effect_date(
invoice.reference_doctype, invoice.reference_name, self.company, self.posting_date
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", self.company, "reconciliation_takes_effect_on"
)
if reconciliation_takes_effect_on == "Advance Payment Date":
posting_date = self.posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
posting_date = frappe.db.get_value(
invoice.reference_doctype, invoice.reference_name, date_field
)
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
posting_date = nowdate()
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
@@ -1585,8 +1604,6 @@ class PaymentEntry(AccountsController):
{
"against_voucher_type": invoice.reference_doctype,
"against_voucher": invoice.reference_name,
"advance_voucher_type": invoice.advance_voucher_type,
"advance_voucher_no": invoice.advance_voucher_no,
"posting_date": posting_date,
}
)
@@ -1611,8 +1628,6 @@ class PaymentEntry(AccountsController):
{
"against_voucher_type": "Payment Entry",
"against_voucher": self.name,
"advance_voucher_type": invoice.advance_voucher_type,
"advance_voucher_no": invoice.advance_voucher_no,
}
)
gle = self.get_gl_dict(
@@ -1761,6 +1776,19 @@ class PaymentEntry(AccountsController):
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
def update_advance_paid(self):
if self.payment_type not in ("Receive", "Pay") or not self.party:
return
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_lazy_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name
self.reference_date = nowdate()

View File

@@ -52,7 +52,7 @@ class TestPaymentEntry(IntegrationTestCase):
self.assertEqual(pe.paid_to_account_type, "Cash")
expected_gle = dict(
(d[0], d) for d in [["Debtors - _TC", 0, 1000, pe.name], ["_Test Cash - _TC", 1000.0, 0, None]]
(d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
)
self.validate_gl_entries(pe.name, expected_gle)
@@ -84,7 +84,7 @@ class TestPaymentEntry(IntegrationTestCase):
expected_gle = dict(
(d[0], d)
for d in [["_Test Receivable USD - _TC", 0, 5500, pe.name], [pe.paid_to, 5500.0, 0, None]]
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
)
self.validate_gl_entries(pe.name, expected_gle)

View File

@@ -22,9 +22,7 @@
"exchange_gain_loss",
"account",
"payment_request",
"payment_request_outstanding",
"advance_voucher_type",
"advance_voucher_no"
"payment_request_outstanding"
],
"fields": [
{
@@ -153,37 +151,20 @@
"fieldtype": "Date",
"label": "Reconcile Effect On",
"read_only": 1
},
{
"columns": 2,
"fieldname": "advance_voucher_type",
"fieldtype": "Link",
"label": "Advance Voucher Type",
"options": "DocType",
"read_only": 1
},
{
"columns": 2,
"fieldname": "advance_voucher_no",
"fieldtype": "Dynamic Link",
"label": "Advance Voucher No",
"options": "advance_voucher_type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-25 04:32:11.040025",
"modified": "2025-01-13 15:56:18.895082",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -16,8 +16,6 @@ class PaymentEntryReference(Document):
account: DF.Link | None
account_type: DF.Data | None
advance_voucher_no: DF.DynamicLink | None
advance_voucher_type: DF.Link | None
allocated_amount: DF.Float
bill_no: DF.Data | None
due_date: DF.Date | None
@@ -28,6 +26,7 @@ class PaymentEntryReference(Document):
parentfield: DF.Data
parenttype: DF.Data
payment_request: DF.Link | None
payment_request_outstanding: DF.Float
payment_term: DF.Link | None
payment_term_outstanding: DF.Float
payment_type: DF.Data | None

View File

@@ -8,14 +8,4 @@ frappe.ui.form.on("Payment Gateway Account", {
frm.set_df_property("payment_gateway", "read_only", 1);
}
},
setup(frm) {
frm.set_query("payment_account", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
},
});

View File

@@ -7,7 +7,6 @@
"field_order": [
"payment_gateway",
"payment_channel",
"company",
"is_default",
"column_break_4",
"payment_account",
@@ -72,21 +71,11 @@
"fieldtype": "Select",
"label": "Payment Channel",
"options": "\nEmail\nPhone\nOther"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-14 16:49:55.210352",
"modified": "2024-03-29 18:53:09.836254",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Gateway Account",
@@ -105,7 +94,6 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -15,7 +15,6 @@ class PaymentGatewayAccount(Document):
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link
currency: DF.ReadOnly | None
is_default: DF.Check
message: DF.SmallText | None
@@ -25,8 +24,7 @@ class PaymentGatewayAccount(Document):
# end: auto-generated types
def autoname(self):
abbr = frappe.db.get_value("Company", self.company, "abbr")
self.name = self.payment_gateway + " - " + self.currency + " - " + abbr
self.name = self.payment_gateway + " - " + self.currency
def validate(self):
self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency")
@@ -36,15 +34,13 @@ class PaymentGatewayAccount(Document):
def update_default_payment_gateway(self):
if self.is_default:
frappe.db.set_value(
"Payment Gateway Account",
{"is_default": 1, "name": ["!=", self.name], "company": self.company},
"is_default",
0,
frappe.db.sql(
"""update `tabPayment Gateway Account` set is_default = 0
where is_default = 1 """
)
def set_as_default_if_not_set(self):
if not frappe.db.exists(
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name), "company": self.company}
if not frappe.db.get_value(
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
):
self.is_default = 1

View File

@@ -197,4 +197,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.gl_entry.gl_entry import (
validate_balance_type,
validate_frozen_account,
)
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, update_voucher_outstanding
from erpnext.accounts.utils import update_voucher_outstanding
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
@@ -51,36 +51,38 @@ class PaymentLedgerEntry(Document):
# end: auto-generated types
def validate_account(self):
account = frappe.get_cached_value(
"Account", self.account, fieldname=["account_type", "company"], as_dict=True
valid_account = frappe.db.get_list(
"Account",
"name",
filters={"name": self.account, "account_type": self.account_type, "company": self.company},
ignore_permissions=True,
)
if account.company != self.company:
frappe.throw(_("{0} account is not of company {1}").format(self.account, self.company))
if account.account_type != self.account_type:
if not valid_account:
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
def validate_account_details(self):
"""Account must be ledger, active and not freezed"""
account = frappe.get_cached_value(
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
)
ret = frappe.db.sql(
"""select is_group, docstatus, company
from tabAccount where name=%s""",
self.account,
as_dict=1,
)[0]
if account.is_group == 1:
if ret.is_group == 1:
frappe.throw(
_(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, self.account)
)
if account.docstatus == 2:
if ret.docstatus == 2:
frappe.throw(
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
)
if account.company != self.company:
if ret.company != self.company:
frappe.throw(
_("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company
@@ -168,7 +170,7 @@ class PaymentLedgerEntry(Document):
# update outstanding amount
if (
self.against_voucher_type in OUTSTANDING_DOCTYPES
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):

View File

@@ -28,7 +28,6 @@
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"sec_break1",
"invoice_name",
"invoices",
@@ -195,12 +194,6 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"depends_on": "eval:doc.party",
"description": "Only 'Payment Entries' made against this advance account are supported.",

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
@@ -393,12 +392,6 @@ class PaymentReconciliation(Document):
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
)
difference_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
)
difference_amount = 0
if frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
@@ -406,14 +399,8 @@ class PaymentReconciliation(Document):
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = flt(
payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
difference_amount_precision,
)
allocated_amount_in_inv_rate = flt(
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
difference_amount_precision,
)
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount
@@ -589,7 +576,6 @@ class PaymentReconciliation(Document):
"difference_amount": flt(row.get("difference_amount")),
"difference_account": row.get("difference_account"),
"difference_posting_date": row.get("gain_loss_posting_date"),
"debit_or_credit_note_posting_date": row.get("debit_or_credit_note_posting_date"),
"cost_center": row.get("cost_center"),
}
)
@@ -603,7 +589,7 @@ class PaymentReconciliation(Document):
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname):
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
def validate_entries(self):
if not self.get("invoices"):
@@ -779,7 +765,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
{
"doctype": "Journal Entry",
"voucher_type": voucher_type,
"posting_date": inv.get("debit_or_credit_note_posting_date") or today(),
"posting_date": today(),
"company": company,
"multi_currency": 1 if inv.currency != company_currency else 0,
"accounts": [
@@ -840,7 +826,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
create_gain_loss_journal(
company,
inv.difference_posting_date,
today(),
inv.party_type,
inv.party,
inv.account,

View File

@@ -1714,67 +1714,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
)
self.assertEqual(len(pl_entries), 3)
def test_advance_payment_reconciliation_date_for_older_date(self):
old_settings = frappe.db.get_value(
"Company",
self.company,
[
"reconciliation_takes_effect_on",
"default_advance_paid_account",
"book_advance_payments_in_separate_party_account",
],
as_dict=True,
)
frappe.db.set_value(
"Company",
self.company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
self.supplier = "_Test Supplier"
pi1 = self.create_purchase_invoice(qty=10, rate=100)
po = self.create_purchase_order(qty=10, rate=100)
pay = get_payment_entry(po.doctype, po.name)
pay.paid_amount = 1000
pay.save().submit()
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Supplier"
pr.party = self.supplier
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.default_advance_account = self.advance_payable_account
pr.get_unreconciled_entries()
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}))
pr.allocation[0].allocated_amount = 100
pr.reconcile()
pay.reload()
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
# test setting of date if not available
frappe.db.set_value("Payment Entry Reference", pay.references[1].name, "reconcile_effect_on", None)
pay.reload()
pay.cancel()
pay.reload()
pi1.reload()
po.reload()
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
pi1.cancel()
po.cancel()
frappe.db.set_value("Company", self.company, old_settings)
def test_advance_payment_reconciliation_against_journal_for_customer(self):
frappe.db.set_value(
"Company",
@@ -2208,138 +2147,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
self.assertEqual(len(pr.get("payments")), 0)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
def test_partial_advance_payment_with_closed_fiscal_year(self):
"""
Test Advance Payment partial reconciliation before period closing and partial after period closing
"""
default_settings = frappe.db.get_value(
"Company",
self.company,
[
"book_advance_payments_in_separate_party_account",
"default_advance_paid_account",
"reconciliation_takes_effect_on",
],
as_dict=True,
)
first_fy_start_date = frappe.db.get_value(
"Fiscal Year", {"disabled": 0}, [{"MIN": "year_start_date"}]
)
prev_fy_start_date = add_years(first_fy_start_date, -1)
prev_fy_end_date = add_days(first_fy_start_date, -1)
create_fiscal_year(
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
)
frappe.db.set_value(
"Company",
self.company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
self.supplier = "_Test Supplier"
# Create advance payment of 1000 (previous FY)
pe = self.create_payment_entry(amount=1000, posting_date=prev_fy_start_date)
pe.party_type = "Supplier"
pe.party = self.supplier
pe.payment_type = "Pay"
pe.paid_from = self.cash
pe.paid_to = self.advance_payable_account
pe.save().submit()
# Create purchase invoice of 600 (previous FY)
pi1 = self.create_purchase_invoice(qty=1, rate=600, do_not_submit=True)
pi1.posting_date = prev_fy_start_date
pi1.set_posting_time = 1
pi1.supplier = self.supplier
pi1.credit_to = self.creditors
pi1.save().submit()
# Reconcile advance payment
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.party = self.supplier
pr.receivable_payable_account = self.creditors
pr.default_advance_account = self.advance_payable_account
pr.from_invoice_date = pr.to_invoice_date = pi1.posting_date
pr.from_payment_date = pr.to_payment_date = pe.posting_date
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi1.name]
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# Verify partial reconciliation
pe.reload()
pi1.reload()
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.references[0].allocated_amount, 600)
self.assertEqual(flt(pe.unallocated_amount), 400)
self.assertEqual(pi1.outstanding_amount, 0)
self.assertEqual(pi1.status, "Paid")
# Close accounting period for March (previous FY)
pcv = make_period_closing_voucher(
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
)
pcv.reload()
self.assertEqual(pcv.gle_processing_status, "Completed")
# Change reconciliation setting to "Reconciliation Date"
frappe.db.set_value(
"Company",
self.company,
"reconciliation_takes_effect_on",
"Reconciliation Date",
)
# Create new purchase invoice for 400 in new fiscal year
pi2 = self.create_purchase_invoice(qty=1, rate=400, do_not_submit=True)
pi2.posting_date = today()
pi2.set_posting_time = 1
pi2.supplier = self.supplier
pi2.currency = "INR"
pi2.credit_to = self.creditors
pi2.save()
pi2.submit()
# Allocate 600 from advance payment to purchase invoice
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.party = self.supplier
pr.receivable_payable_account = self.creditors
pr.default_advance_account = self.advance_payable_account
pr.from_invoice_date = pr.to_invoice_date = pi2.posting_date
pr.from_payment_date = pr.to_payment_date = pe.posting_date
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi2.name]
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
pe.reload()
pi2.reload()
# Assert advance payment is fully allocated
self.assertEqual(len(pe.references), 2)
self.assertEqual(flt(pe.unallocated_amount), 0)
# Assert new invoice is fully paid
self.assertEqual(pi2.outstanding_amount, 0)
self.assertEqual(pi2.status, "Paid")
# Verify reconciliation dates are correct based on company setting
self.assertEqual(getdate(pe.references[0].reconcile_effect_on), getdate(pi1.posting_date))
self.assertEqual(getdate(pe.references[1].reconcile_effect_on), getdate(pi2.posting_date))
frappe.db.set_value("Company", self.company, default_settings)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -20,7 +20,6 @@
"section_break_5",
"difference_amount",
"gain_loss_posting_date",
"debit_or_credit_note_posting_date",
"column_break_7",
"difference_account",
"exchange_rate",
@@ -169,25 +168,19 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "debit_or_credit_note_posting_date",
"fieldtype": "Date",
"label": "Debit / Credit Note Posting Date"
}
],
"is_virtual": 1,
"istable": 1,
"links": [],
"modified": "2025-08-20 19:12:50.406769",
"modified": "2024-03-27 13:10:10.704417",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -18,7 +18,6 @@ class PaymentReconciliationAllocation(Document):
amount: DF.Currency
cost_center: DF.Link | None
currency: DF.Link | None
debit_or_credit_note_posting_date: DF.Date | None
difference_account: DF.Link | None
difference_amount: DF.Currency
exchange_rate: DF.Float

View File

@@ -9,14 +9,6 @@ frappe.ui.form.on("Payment Request", {
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
};
});
frm.set_query("payment_gateway_account", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
},
});

View File

@@ -228,8 +228,7 @@
"fetch_from": "bank_account.iban",
"fieldname": "iban",
"fieldtype": "Read Only",
"label": "IBAN",
"options": "IBAN"
"label": "IBAN"
},
{
"fetch_from": "bank_account.branch_code",
@@ -459,12 +458,11 @@
"label": "Phone Number"
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-08-29 11:52:48.555415",
"modified": "2025-01-04 05:39:32.448857",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -499,9 +497,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_preview_popup": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes, get_currency_precision
from erpnext.accounts.utils import get_account_currency, get_currency_precision
from erpnext.utilities import payment_app_import_guard
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
@@ -129,13 +129,7 @@ class PaymentRequest(Document):
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
if (
flt(
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
get_currency_precision(),
)
> ref_amount
):
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype
@@ -470,7 +464,10 @@ class PaymentRequest(Document):
return create_stripe_subscription(gateway_controller, data)
def update_reference_advance_payment_status(self):
if self.reference_doctype in get_advance_payment_doctypes():
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.reference_doctype in advance_payment_doctypes:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
ref_doc.set_advance_payment_status()
@@ -540,8 +537,7 @@ def make_payment_request(**args):
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company
gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
@@ -788,7 +784,7 @@ def get_gateway_details(args): # nosemgrep
"""
Return gateway and payment account of default payment gateway
"""
gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company})
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
return get_payment_gateway_account(gateway_account)
@@ -830,7 +826,8 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
if not references:
return
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
precision = references[0].precision("allocated_amount")
referenced_payment_requests = frappe.get_all(
"Payment Request",
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},

View File

@@ -34,14 +34,12 @@ payment_method = [
"payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank - _TC",
"currency": "INR",
"company": "_Test Company",
},
{
"doctype": "Payment Gateway Account",
"payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank USD - _TC",
"currency": "USD",
"company": "_Test Company",
},
{
"doctype": "Payment Gateway Account",
@@ -49,7 +47,6 @@ payment_method = [
"payment_account": "_Test Bank USD - _TC",
"payment_channel": "Other",
"currency": "USD",
"company": "_Test Company",
},
{
"doctype": "Payment Gateway Account",
@@ -57,7 +54,6 @@ payment_method = [
"payment_account": "_Test Bank USD - _TC",
"payment_channel": "Phone",
"currency": "USD",
"company": "_Test Company",
},
]
@@ -71,11 +67,7 @@ class TestPaymentRequest(IntegrationTestCase):
for method in payment_method:
if not frappe.db.get_value(
"Payment Gateway Account",
{
"payment_gateway": method["payment_gateway"],
"currency": method["currency"],
"company": method["company"],
},
{"payment_gateway": method["payment_gateway"], "currency": method["currency"]},
"name",
):
frappe.get_doc(method).insert(ignore_permissions=True)
@@ -111,7 +103,7 @@ class TestPaymentRequest(IntegrationTestCase):
dt="Sales Order",
dn=so_inr.name,
recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - INR - _TC",
payment_gateway_account="_Test Gateway - INR",
)
self.assertEqual(pr.reference_doctype, "Sales Order")
@@ -125,7 +117,7 @@ class TestPaymentRequest(IntegrationTestCase):
dt="Sales Invoice",
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - USD - _TC",
payment_gateway_account="_Test Gateway - USD",
)
self.assertEqual(pr.reference_doctype, "Sales Invoice")
@@ -138,7 +130,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway Other - USD - _TC",
payment_gateway_account="_Test Gateway Other - USD",
submit_doc=True,
return_doc=True,
)
@@ -153,7 +145,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
payment_gateway_account="_Test Gateway - USD", # email channel
submit_doc=False,
return_doc=True,
)
@@ -171,7 +163,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway Phone - USD - _TC",
payment_gateway_account="_Test Gateway Phone - USD",
submit_doc=True,
return_doc=True,
)
@@ -188,7 +180,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
payment_gateway_account="_Test Gateway - USD", # email channel
submit_doc=True,
return_doc=True,
)
@@ -209,7 +201,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
payment_gateway_account="_Test Gateway - USD", # email channel
make_sales_invoice=True,
mute_email=True,
submit_doc=True,
@@ -240,7 +232,7 @@ class TestPaymentRequest(IntegrationTestCase):
party="_Test Supplier USD",
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD - _TC",
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
@@ -265,7 +257,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD - _TC",
payment_gateway_account="_Test Gateway - USD",
return_doc=1,
)
@@ -284,7 +276,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD - _TC",
payment_gateway_account="_Test Gateway - USD",
return_doc=1,
)
@@ -308,7 +300,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=so_inr.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - INR - _TC",
payment_gateway_account="_Test Gateway - INR",
submit_doc=1,
return_doc=1,
)
@@ -330,7 +322,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD - _TC",
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
@@ -374,7 +366,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD - _TC",
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
@@ -479,7 +471,7 @@ class TestPaymentRequest(IntegrationTestCase):
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 800)
self.assertEqual(pe.references[0].outstanding_amount, 0) # Also for orders it will zero
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
@@ -821,33 +813,3 @@ class TestPaymentRequest(IntegrationTestCase):
pi.load_from_db()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
def test_payment_request_on_unreconcile(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
pi.submit()
pr = make_payment_request(
dt=pi.doctype,
dn=pi.name,
mute_email=1,
submit_doc=True,
return_doc=True,
)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
pe = pr.create_payment_entry()
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": pe.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
unreconcile.submit()
pi.load_from_db()
pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount)

View File

@@ -10,19 +10,14 @@
"description",
"section_break_4",
"due_date",
"invoice_portion",
"mode_of_payment",
"column_break_5",
"due_date_based_on",
"credit_days",
"credit_months",
"invoice_portion",
"section_break_6",
"discount_date",
"discount",
"discount_type",
"discount_date",
"column_break_9",
"discount_validity_based_on",
"discount_validity",
"discount",
"section_break_9",
"payment_amount",
"outstanding",
@@ -177,50 +172,12 @@
"label": "Paid Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"label": "Credit Days",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"label": "Credit Months",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "discount",
"fieldname": "discount_validity_based_on",
"fieldtype": "Select",
"label": "Discount Validity Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "discount_validity_based_on",
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-31 08:38:25.820701",
"modified": "2025-03-11 11:06:51.792982",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@@ -232,4 +189,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,27 +17,12 @@ class PaymentSchedule(Document):
base_outstanding: DF.Currency
base_paid_amount: DF.Currency
base_payment_amount: DF.Currency
credit_days: DF.Int
credit_months: DF.Int
description: DF.SmallText | None
discount: DF.Float
discount_date: DF.Date | None
discount_type: DF.Literal["Percentage", "Amount"]
discount_validity: DF.Int
discount_validity_based_on: DF.Literal[
"",
"Day(s) after invoice date",
"Day(s) after the end of the invoice month",
"Month(s) after the end of the invoice month",
]
discounted_amount: DF.Currency
due_date: DF.Date
due_date_based_on: DF.Literal[
"",
"Day(s) after invoice date",
"Day(s) after the end of the invoice month",
"Month(s) after the end of the invoice month",
]
invoice_portion: DF.Percent
mode_of_payment: DF.Link | None
outstanding: DF.Currency

View File

@@ -162,4 +162,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -75,17 +75,6 @@ class PeriodClosingVoucher(AccountsController):
return
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
previous_fiscal_year_closed = frappe.db.exists(
"Period Closing Voucher",
{
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
"docstatus": 1,
"company": self.company,
},
)
if previous_fiscal_year_closed:
return
gle_exists_in_previous_year = frappe.db.exists(
"GL Entry",
{
@@ -97,7 +86,16 @@ class PeriodClosingVoucher(AccountsController):
if not gle_exists_in_previous_year:
return
frappe.throw(_("Previous Year is not closed, please close it first"))
previous_fiscal_year_closed = frappe.db.exists(
"Period Closing Voucher",
{
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
"docstatus": 1,
"company": self.company,
},
)
if not previous_fiscal_year_closed:
frappe.throw(_("Previous Year is not closed, please close it first"))
def block_if_future_closing_voucher_exists(self):
future_closing_voucher = self.get_future_closing_voucher()
@@ -212,10 +210,8 @@ class PeriodClosingVoucher(AccountsController):
return gl_entry
def get_gle_for_closing_account(self, dimension_balance, dimensions):
balance_in_account_currency = flt(dimension_balance.balance_in_account_currency)
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
debit = balance_in_company_currency if balance_in_company_currency > 0 else 0
credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0
gl_entry = frappe._dict(
{
"company": self.company,
@@ -224,10 +220,14 @@ class PeriodClosingVoucher(AccountsController):
"account_currency": frappe.db.get_value(
"Account", self.closing_account_head, "account_currency"
),
"debit_in_account_currency": debit,
"debit": debit,
"credit_in_account_currency": credit,
"credit": credit,
"debit_in_account_currency": balance_in_account_currency
if balance_in_account_currency > 0
else 0,
"debit": balance_in_company_currency if balance_in_company_currency > 0 else 0,
"credit_in_account_currency": abs(balance_in_account_currency)
if balance_in_account_currency < 0
else 0,
"credit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
"is_period_closing_voucher_entry": 1,
"voucher_type": "Period Closing Voucher",
"voucher_no": self.name,

View File

@@ -74,7 +74,6 @@
"label": "User Details"
},
{
"fetch_from": "pos_opening_entry.company",
"fetch_if_empty": 1,
"fieldname": "company",
"fieldtype": "Link",
@@ -104,7 +103,6 @@
"fieldtype": "Link",
"label": "Cashier",
"options": "User",
"read_only": 1,
"reqd": 1
},
{
@@ -261,7 +259,7 @@
"link_fieldname": "pos_closing_entry"
}
],
"modified": "2025-06-14 02:38:14.962291",
"modified": "2025-06-06 12:00:31.955176",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",

View File

@@ -209,16 +209,13 @@ class POSClosingEntry(StatusUpdater):
def on_submit(self):
consolidate_pos_invoices(closing_entry=self)
frappe.publish_realtime(
f"poe_{self.pos_opening_entry}",
message={"operation": "Closed", "doc": self},
f"poe_{self.pos_opening_entry}_closed",
self,
docname=f"POS Opening Entry/{self.pos_opening_entry}",
)
self.update_sales_invoices_closing_entry()
def before_cancel(self):
self.check_pce_is_cancellable()
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
@@ -240,15 +237,6 @@ class POSClosingEntry(StatusUpdater):
"Sales Invoice", d.sales_invoice, "pos_closing_entry", self.name if not cancel else None
)
def check_pce_is_cancellable(self):
if frappe.db.exists("POS Opening Entry", {"pos_profile": self.pos_profile, "status": "Open"}):
frappe.throw(
title=_("Cannot cancel POS Closing Entry"),
msg=_(
"POS Profile - {0} is currently open. Please close the POS or cancel the existing POS Opening Entry before cancelling this POS Closing Entry."
).format(frappe.bold(self.pos_profile)),
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -279,7 +267,7 @@ def get_invoices(start, end, pos_profile, user):
def get_payments(invoices):
if not len(invoices):
return []
return
invoices_name = [d.name for d in invoices]
@@ -313,7 +301,7 @@ def get_payments(invoices):
def get_taxes(invoices):
if not len(invoices):
return []
return
invoices_name = [d.name for d in invoices]

View File

@@ -14,7 +14,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
}
company() {
erpnext.utils.set_letter_head(this.frm);
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
this.frm.set_value("set_warehouse", "");
this.frm.set_value("taxes_and_charges", "");
@@ -55,16 +54,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
});
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
if (this.frm.doc.pos_profile) {
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
.then((r) => {
if (!r.exc) {
this.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
}
});
}
}
onload_post_render(frm) {
@@ -77,13 +66,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
if (doc.docstatus == 1 && !doc.is_return) {
this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create"));
if (["Partly Paid", "Overdue", "Unpaid"].includes(doc.status)) {
this.frm.add_custom_button(
__("Payment"),
this.collect_outstanding_payment.bind(this),
__("Create")
);
}
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -130,7 +112,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
this.frm.meta.default_print_format = r.message.print_format || "";
this.frm.doc.campaign = r.message.campaign;
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
this.frm.set_default_payment = r.message.set_default_payment;
}
this.frm.script_manager.trigger("update_stock");
this.calculate_taxes_and_totals();
@@ -229,138 +210,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
frm: this.frm,
});
}
async collect_outstanding_payment() {
const total_amount = flt(this.frm.doc.rounded_total) | flt(this.frm.doc.grand_total);
const paid_amount = flt(this.frm.doc.paid_amount);
const outstanding_amount = flt(this.frm.doc.outstanding_amount);
const me = this;
const table_fields = [
{
fieldname: "mode_of_payment",
fieldtype: "Link",
in_list_view: 1,
label: __("Mode of Payment"),
options: "Mode of Payment",
reqd: 1,
},
{
fieldname: "amount",
fieldtype: "Currency",
in_list_view: 1,
label: __("Amount"),
options: this.frm.doc.currency,
reqd: 1,
onchange: function () {
dialog.fields_dict.payments.df.data.some((d) => {
if (d.idx == this.doc.idx) {
d.amount = this.value === null ? 0 : this.value;
dialog.fields_dict.payments.grid.refresh();
return true;
}
});
let amount = 0;
for (let d of dialog.fields_dict.payments.df.data) {
amount += d.amount;
}
let change_amount = total_amount - (paid_amount + amount);
dialog.fields_dict.outstanding_amount.set_value(
outstanding_amount - amount < 0 ? 0 : outstanding_amount - amount
);
dialog.fields_dict.paid_amount.set_value(paid_amount + amount);
dialog.fields_dict.change_amount.set_value(change_amount < 0 ? change_amount * -1 : 0);
},
},
];
const payment_method_data = await this.fetch_pos_payment_methods();
const dialog = new frappe.ui.Dialog({
title: __("Collect Outstanding Amount"),
fields: [
{
fieldname: "payments",
fieldtype: "Table",
label: __("Payments"),
cannot_add_rows: false,
in_place_edit: true,
reqd: 1,
data: payment_method_data,
fields: table_fields,
},
{
fieldname: "section_break_1",
fieldtype: "Section Break",
},
{
fieldname: "outstanding_amount",
fieldtype: "Currency",
label: __("Outstanding Amount"),
read_only: 1,
default: outstanding_amount,
},
{
fieldname: "column_break_1",
fieldtype: "Column Break",
},
{
fieldname: "paid_amount",
fieldtype: "Currency",
label: __("Paid Amount"),
read_only: 1,
default: paid_amount,
},
{
fieldname: "change_amount",
fieldtype: "Currency",
label: __("Change Amount"),
read_only: 1,
default: 0,
},
],
primary_action_label: __("Submit"),
primary_action(values) {
dialog.hide();
me.frm.call({
doc: me.frm.doc,
method: "update_payments",
args: {
payments: values.payments.filter((d) => d.amount != 0),
},
freeze: true,
callback: function (r) {
if (!r.exc) {
frappe.show_alert({
message: __("Payments updated."),
indicator: "green",
});
me.frm.reload_doc();
} else {
frappe.show_alert({
message: __("Payments could not be updated."),
indicator: "red",
});
}
},
});
},
});
dialog.show();
}
async fetch_pos_payment_methods() {
const pos_profile = this.frm.doc.pos_profile;
if (!pos_profile) return;
const pos_profile_doc = await frappe.db.get_doc("POS Profile", pos_profile);
const data = [];
pos_profile_doc.payments.forEach((pay) => {
const { mode_of_payment } = pay;
data.push({ mode_of_payment, amount: 0 });
});
return data;
}
};
extend_cscript(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }));

View File

@@ -30,6 +30,24 @@
"project",
"dimension_col_break",
"cost_center",
"customer_po_details",
"po_no",
"column_break_23",
"po_date",
"address_and_contact",
"customer_address",
"address_display",
"contact_person",
"contact_display",
"contact_mobile",
"contact_email",
"territory",
"col_break4",
"shipping_address_name",
"shipping_address",
"company_address",
"company_address_display",
"company_contact_person",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -43,7 +61,6 @@
"items_section",
"update_stock",
"scan_barcode",
"last_scanned_warehouse",
"items",
"pricing_rule_details",
"pricing_rules",
@@ -74,6 +91,14 @@
"base_total_taxes_and_charges",
"column_break_47",
"total_taxes_and_charges",
"loyalty_points_redemption",
"loyalty_points",
"loyalty_amount",
"redeem_loyalty_points",
"column_break_77",
"loyalty_program",
"loyalty_redemption_account",
"loyalty_redemption_cost_center",
"section_break_49",
"coupon_code",
"apply_discount_on",
@@ -93,7 +118,13 @@
"in_words",
"total_advance",
"outstanding_amount",
"payments_tab",
"advances_section",
"allocate_advances_automatically",
"get_advances",
"advances",
"payment_schedule_section",
"payment_terms_template",
"payment_schedule",
"payments_section",
"cash_bank_account",
"payments",
@@ -106,10 +137,6 @@
"column_break_90",
"change_amount",
"account_for_change_amount",
"advances_section",
"allocate_advances_automatically",
"get_advances",
"advances",
"column_break4",
"write_off_amount",
"base_write_off_amount",
@@ -117,41 +144,9 @@
"column_break_74",
"write_off_account",
"write_off_cost_center",
"loyalty_points_redemption",
"loyalty_points",
"loyalty_amount",
"redeem_loyalty_points",
"column_break_77",
"loyalty_program",
"loyalty_redemption_account",
"loyalty_redemption_cost_center",
"contact_and_address_tab",
"address_and_contact",
"customer_address",
"address_display",
"contact_person",
"contact_display",
"contact_mobile",
"contact_email",
"territory",
"col_break4",
"shipping_address_name",
"shipping_address",
"company_address",
"company_address_display",
"company_contact_person",
"terms_tab",
"payment_schedule_section",
"payment_terms_template",
"payment_schedule",
"terms_section_break",
"tc_name",
"terms",
"more_info_tab",
"customer_po_details",
"po_no",
"column_break_23",
"po_date",
"edit_printing_settings",
"letter_head",
"group_same_items",
@@ -297,7 +292,6 @@
"search_index": 1
},
{
"default": "Now",
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
@@ -404,6 +398,7 @@
"label": "Customer's Purchase Order Date"
},
{
"collapsible": 1,
"fieldname": "address_and_contact",
"fieldtype": "Section Break",
"label": "Address and Contact"
@@ -1055,6 +1050,7 @@
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)",
"fieldname": "payment_schedule_section",
"fieldtype": "Section Break",
@@ -1134,10 +1130,8 @@
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_88",
"fieldtype": "Section Break",
"label": "Changes"
"fieldtype": "Section Break"
},
{
"depends_on": "is_pos",
@@ -1224,6 +1218,7 @@
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "terms",
"fieldname": "terms_section_break",
"fieldtype": "Section Break",
@@ -1335,7 +1330,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nPartly Paid\nUnpaid\nPartly Paid and Discounted\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
"options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
"print_hide": 1,
"read_only": 1
},
@@ -1421,8 +1416,6 @@
"width": "50%"
},
{
"fetch_from": "sales_partner.commission_rate",
"fetch_if_empty": 1,
"fieldname": "commission_rate",
"fieldtype": "Float",
"label": "Commission Rate (%)",
@@ -1575,39 +1568,12 @@
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
},
{
"fieldname": "contact_and_address_tab",
"fieldtype": "Tab Break",
"label": "Address & Contact"
},
{
"fieldname": "terms_tab",
"fieldtype": "Tab Break",
"label": "Terms"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"depends_on": "eval: doc.last_scanned_warehouse",
"fieldname": "last_scanned_warehouse",
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 22:22:31.471752",
"modified": "2025-01-06 15:03:19.957277",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1652,7 +1618,6 @@
"role": "All"
}
],
"row_format": "Dynamic",
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
"show_name_in_global_search": 1,
"sort_field": "creation",

View File

@@ -149,9 +149,7 @@ class POSInvoice(SalesInvoice):
"Consolidated",
"Submitted",
"Paid",
"Partly Paid",
"Unpaid",
"Partly Paid and Discounted",
"Unpaid and Discounted",
"Overdue and Discounted",
"Overdue",
@@ -217,15 +215,11 @@ class POSInvoice(SalesInvoice):
self.validate_loyalty_transaction()
self.validate_company_with_pos_company()
self.validate_full_payment()
self.update_packing_list()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
validate_coupon_code(self.coupon_code)
def before_submit(self):
self.set_outstanding_amount()
def on_submit(self):
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
if not self.is_return and self.loyalty_program:
@@ -379,6 +373,18 @@ class POSInvoice(SalesInvoice):
_("Payment related to {0} is not completed").format(pay.mode_of_payment)
)
def validate_pos_opening_entry(self):
opening_entries = frappe.get_list(
"POS Opening Entry", filters={"pos_profile": self.pos_profile, "status": "Open", "docstatus": 1}
)
if len(opening_entries) == 0:
frappe.throw(
title=_("POS Opening Entry Missing"),
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
frappe.bold(self.pos_profile)
),
)
def validate_stock_availablility(self):
if self.is_return:
return
@@ -411,9 +417,9 @@ class POSInvoice(SalesInvoice):
)
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
d.idx, item_code, warehouse
),
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
).format(d.idx, item_code, warehouse, available_stock),
title=_("Item Unavailable"),
)
@@ -531,10 +537,6 @@ class POSInvoice(SalesInvoice):
)
)
def set_outstanding_amount(self):
total = flt(self.rounded_total) or flt(self.grand_total)
self.outstanding_amount = total - flt(self.paid_amount) if total > flt(self.paid_amount) else 0
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
@@ -556,8 +558,6 @@ class POSInvoice(SalesInvoice):
self.status = "Draft"
return
total = flt(self.rounded_total) or flt(self.grand_total)
if not status:
if self.docstatus == 2:
status = "Cancelled"
@@ -573,14 +573,6 @@ class POSInvoice(SalesInvoice):
self.status = "Overdue and Discounted"
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()):
self.status = "Overdue"
elif (
0 < flt(self.outstanding_amount) < total
and self.is_discounted
and self.get_discounting_status() == "Disbursed"
):
self.status = "Partly Paid and Discounted"
elif 0 < flt(self.outstanding_amount) < total:
self.status = "Partly Paid"
elif (
flt(self.outstanding_amount) > 0
and getdate(self.due_date) >= getdate(nowdate())
@@ -717,13 +709,7 @@ class POSInvoice(SalesInvoice):
"Account", self.debit_to, "account_currency"
)
if not self.due_date and self.customer:
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
super(SalesInvoice, self).set_missing_values(for_validate)
@@ -738,7 +724,6 @@ class POSInvoice(SalesInvoice):
"utm_campaign": profile.get("utm_campaign"),
"utm_medium": profile.get("utm_medium"),
"allow_print_before_pay": profile.get("allow_print_before_pay"),
"set_default_payment": profile.get("set_grand_total_to_default_mop"),
}
@frappe.whitelist()
@@ -808,48 +793,6 @@ class POSInvoice(SalesInvoice):
if pr:
return frappe.get_doc("Payment Request", pr)
@frappe.whitelist()
def update_payments(self, payments):
if self.status == "Consolidated":
frappe.throw(_("Create Payment Entry for Consolidated POS Invoices."))
paid_amount = flt(self.paid_amount)
total = flt(self.rounded_total) or flt(self.grand_total)
if paid_amount >= total:
frappe.throw(title=_("Invoice Paid"), msg=_("This invoice has already been paid."))
idx = self.payments[-1].idx if self.payments else -1
for d in payments:
idx += 1
payment = create_payments_on_invoice(self, idx, frappe._dict(d))
paid_amount += flt(payment.amount)
payment.submit()
paid_amount = flt(flt(paid_amount), self.precision("paid_amount"))
base_paid_amount = flt(flt(paid_amount * self.conversion_rate), self.precision("base_paid_amount"))
outstanding_amount = (
flt(flt(total - paid_amount), self.precision("outstanding_amount")) if total > paid_amount else 0
)
change_amount = (
flt(flt(paid_amount - total), self.precision("change_amount")) if paid_amount > total else 0
)
pi = frappe.qb.DocType("POS Invoice")
query = (
frappe.qb.update(pi)
.set(pi.paid_amount, paid_amount)
.set(pi.base_paid_amount, base_paid_amount)
.set(pi.outstanding_amount, outstanding_amount)
.set(pi.change_amount, change_amount)
.where(pi.name == self.name)
)
query.run()
self.reload()
self.set_status(update=True)
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
@@ -875,8 +818,10 @@ def get_bundle_availability(bundle_item_code, warehouse):
bundle_bin_qty = 1000000
for item in product_bundle.items:
item_bin_qty = get_bin_qty(item.item_code, warehouse)
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = item_bin_qty / item.qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):
@@ -899,49 +844,13 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse):
"""
Calculate total quantity reserved for the given item and warehouse.
Includes:
- Direct sales of the item in submitted POS Invoices
- Sales of the item as a component of a Product Bundle
Excludes consolidated invoices (already merged into Sales Invoices via
POS Closing Entry). Used to reflect near real-time availability in the
POS UI and to prevent overselling while multiple sessions may be active.
"""
pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse)
packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse)
reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty
return reserved_qty
def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
"""
Get the total reserved quantity for a given item in POS Invoices
from a specific child table.
Args:
child_table (str): Name of the child table to query
(e.g., "POS Invoice Item", "Packed Item").
item_code (str): The Item Code to filter by.
warehouse (str): The Warehouse to filter by.
Returns:
float: The total reserved quantity for the item in the given
warehouse from submitted, unconsolidated POS Invoices.
"""
p_inv = frappe.qb.DocType("POS Invoice")
p_item = frappe.qb.DocType(child_table)
qty_column = "qty" if child_table == "Packed Item" else "stock_qty"
p_item = frappe.qb.DocType("POS Invoice Item")
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item[qty_column]).as_("stock_qty"))
.select(Sum(p_item.stock_qty).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
@@ -1035,19 +944,3 @@ def get_item_group(pos_profile):
item_groups.extend(get_descendants_of("Item Group", row.item_group))
return list(set(item_groups))
def create_payments_on_invoice(doc, idx, payment_details):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
payment = frappe.new_doc("Sales Invoice Payment")
payment.idx = idx
payment.mode_of_payment = payment_details.mode_of_payment
payment.amount = payment_details.amount
payment.base_amount = payment.amount * doc.conversion_rate
payment.parent = doc.name
payment.parentfield = "payments"
payment.parenttype = doc.doctype
payment.account = get_bank_cash_account(payment.mode_of_payment, doc.company).get("account")
return payment

View File

@@ -18,13 +18,11 @@ frappe.listview_settings["POS Invoice"] = {
Draft: "red",
Unpaid: "orange",
Paid: "green",
"Partly Paid": "yellow",
Submitted: "blue",
Consolidated: "green",
Return: "darkgrey",
"Unpaid and Discounted": "orange",
"Overdue and Discounted": "red",
"Partly Paid and Discounted": "yellow",
Overdue: "red",
};
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];

View File

@@ -401,50 +401,6 @@ class TestPOSInvoice(IntegrationTestCase):
pos_inv.insert()
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
def test_partly_paid_invoices(self):
set_allow_partial_payment(self.pos_profile, 1)
pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1)
pos_inv.append(
"payments",
{"mode_of_payment": "Cash", "amount": 90},
)
pos_inv.save()
pos_inv.submit()
self.assertEqual(pos_inv.paid_amount, 90)
self.assertEqual(pos_inv.status, "Partly Paid")
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 10}])
self.assertEqual(pos_inv.paid_amount, 100)
self.assertEqual(pos_inv.status, "Paid")
set_allow_partial_payment(self.pos_profile, 0)
def test_multi_payment_for_partly_paid_invoices(self):
set_allow_partial_payment(self.pos_profile, 1)
pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1)
pos_inv.append(
"payments",
{"mode_of_payment": "Cash", "amount": 90},
)
pos_inv.save()
pos_inv.submit()
self.assertEqual(pos_inv.paid_amount, 90)
self.assertEqual(pos_inv.status, "Partly Paid")
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}])
self.assertEqual(pos_inv.paid_amount, 95)
self.assertEqual(pos_inv.status, "Partly Paid")
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}])
self.assertEqual(pos_inv.paid_amount, 100)
self.assertEqual(pos_inv.status, "Paid")
set_allow_partial_payment(self.pos_profile, 0)
def test_serialized_item_transaction(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -1133,7 +1089,8 @@ def create_pos_invoice(**args):
return pos_inv
def set_allow_partial_payment(pos_profile, value):
pos_profile.reload()
pos_profile.allow_partial_payment = value
pos_profile.save()
def make_batch_item(item_name):
from erpnext.stock.doctype.item.test_item import make_item
if not frappe.db.exists(item_name):
return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1))

View File

@@ -5,7 +5,6 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"posting_date",
"posting_time",
"merge_invoices_based_on",
@@ -114,22 +113,12 @@
"label": "Posting Time",
"no_copy": 1,
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-07-02 17:08:04.747202",
"modified": "2024-03-27 13:10:15.620564",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",
@@ -190,9 +179,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -29,10 +29,11 @@ class POSInvoiceMergeLog(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
POSInvoiceReference,
)
amended_from: DF.Link | None
company: DF.Link
consolidated_credit_note: DF.Link | None
consolidated_invoice: DF.Link | None
customer: DF.Link
@@ -338,11 +339,6 @@ class POSInvoiceMergeLog(Document):
invoice.flags.ignore_pos_profile = True
invoice.pos_profile = ""
# Unset Commission Section
invoice.set("sales_partner", None)
invoice.set("commission_rate", 0)
invoice.set("total_commission", 0)
return invoice
def get_new_sales_invoice(self):
@@ -588,7 +584,6 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.company = closing_entry.get("company") if closing_entry else None
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.set("pos_invoices", _invoices)

View File

@@ -491,26 +491,3 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice)
def test_company_in_pos_invoice_merge_log(self):
"""
Test if the company is fetched from POS Closing Entry
"""
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.save()
pos_inv.submit()
closing_entry = make_closing_entry_from_opening(opening_entry)
closing_entry.insert()
closing_entry.submit()
self.assertTrue(frappe.db.exists("POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}))
pos_merge_log_company = frappe.db.get_value(
"POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}, "company"
)
self.assertEqual(pos_merge_log_company, closing_entry.company)

View File

@@ -37,8 +37,6 @@ class POSOpeningEntry(StatusUpdater):
def validate(self):
self.validate_pos_profile_and_cashier()
self.check_open_pos_exists()
self.check_user_already_assigned()
self.validate_payment_method_account()
self.set_status()
@@ -51,22 +49,6 @@ class POSOpeningEntry(StatusUpdater):
if not cint(frappe.db.get_value("User", self.user, "enabled")):
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
def check_open_pos_exists(self):
if frappe.db.exists("POS Opening Entry", {"pos_profile": self.pos_profile, "status": "Open"}):
frappe.throw(
title=_("POS Opening Entry Exists"),
msg=_(
"{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry."
).format(frappe.bold(self.pos_profile)),
)
def check_user_already_assigned(self):
if frappe.db.exists("POS Opening Entry", {"user": self.user, "status": "Open"}):
frappe.throw(
title=_("Cannot Assign Cashier"),
msg=_("Cashier is currently assigned to another POS."),
)
def validate_payment_method_account(self):
invalid_modes = []
for d in self.balance_details:
@@ -89,25 +71,5 @@ class POSOpeningEntry(StatusUpdater):
def on_submit(self):
self.set_status(update=True)
def before_cancel(self):
self.check_poe_is_cancellable()
def on_cancel(self):
self.set_status(update=True)
frappe.publish_realtime(
f"poe_{self.name}",
message={"operation": "Cancelled"},
docname=f"POS Opening Entry/{self.name}",
)
def check_poe_is_cancellable(self):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import get_invoices
invoices = get_invoices(
self.period_start_date, frappe.utils.get_datetime(), self.pos_profile, self.user
)
if invoices.get("invoices"):
frappe.throw(
title=_("POS Opening Entry Cancellation Error"),
msg=_("POS Opening Entry cannot be cancelled as unconsolidated Invoices exists."),
)

View File

@@ -3,107 +3,14 @@
import unittest
import frappe
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestPOSOpeningEntry(IntegrationTestCase):
@classmethod
def setUpClass(cls):
frappe.db.sql("delete from `tabPOS Opening Entry`")
cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"}))
@classmethod
def tearDownClass(cls):
frappe.db.sql("delete from `tabPOS Opening Entry`")
def setUp(self):
# Make stock available for POS Sales
frappe.db.sql("delete from `tabPOS Opening Entry`")
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
self.init_user_and_profile = init_user_and_profile
def tearDown(self):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
def test_pos_opening_entry(self):
test_user, pos_profile = self.init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
self.assertEqual(opening_entry.status, "Open")
self.assertNotEqual(opening_entry.docstatus, 0)
def test_multiple_pos_opening_entries_for_same_pos_profile(self):
test_user, pos_profile = self.init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
self.assertEqual(opening_entry.status, "Open")
with self.assertRaises(frappe.ValidationError):
create_opening_entry(pos_profile, test_user.name)
def test_multiple_pos_opening_entry_for_multiple_pos_profiles(self):
test_user, pos_profile = self.init_user_and_profile()
opening_entry_1 = create_opening_entry(pos_profile, test_user.name)
self.assertEqual(opening_entry_1.status, "Open")
self.assertEqual(opening_entry_1.user, test_user.name)
cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager")
frappe.set_user(cashier_user.name)
pos_profile2 = make_pos_profile(name="_Test POS Profile 2")
opening_entry_2 = create_opening_entry(pos_profile2, cashier_user.name)
self.assertEqual(opening_entry_2.status, "Open")
self.assertEqual(opening_entry_2.user, cashier_user.name)
def test_multiple_pos_opening_entry_for_same_pos_profile_by_multiple_user(self):
test_user, pos_profile = self.init_user_and_profile()
cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager")
opening_entry = create_opening_entry(pos_profile, test_user.name)
self.assertEqual(opening_entry.status, "Open")
with self.assertRaises(frappe.ValidationError):
create_opening_entry(pos_profile, cashier_user.name)
def test_user_assignment_to_multiple_pos_profile(self):
test_user, pos_profile = self.init_user_and_profile()
opening_entry_1 = create_opening_entry(pos_profile, test_user.name)
self.assertEqual(opening_entry_1.user, test_user.name)
pos_profile2 = make_pos_profile(name="_Test POS Profile 2")
with self.assertRaises(frappe.ValidationError):
create_opening_entry(pos_profile2, test_user.name)
def test_cancel_pos_opening_entry_without_invoices(self):
test_user, pos_profile = self.init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True)
opening_entry.cancel()
self.assertEqual(opening_entry.status, "Cancelled")
self.assertNotEqual(opening_entry.docstatus, 1)
def test_cancel_pos_opening_entry_with_invoice(self):
test_user, pos_profile = self.init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True)
pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
pos_inv1.save()
pos_inv1.submit()
self.assertRaises(frappe.ValidationError, opening_entry.cancel)
pass
def create_opening_entry(pos_profile, user, get_obj=False):
def create_opening_entry(pos_profile, user):
entry = frappe.new_doc("POS Opening Entry")
entry.pos_profile = pos_profile.name
entry.user = user
@@ -117,7 +24,4 @@ def create_opening_entry(pos_profile, user, get_obj=False):
entry.set("balance_details", balance_details)
entry.submit()
if get_obj:
return entry
return entry.as_dict()

View File

@@ -135,7 +135,6 @@ frappe.ui.form.on("POS Profile", {
company: function (frm) {
frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
toggle_display_account_head: function (frm) {

View File

@@ -26,14 +26,13 @@
"auto_add_item_to_cart",
"validate_stock_on_save",
"print_receipt_on_order_complete",
"action_on_new_invoice",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
"allow_rate_change",
"allow_discount_change",
"set_grand_total_to_default_mop",
"allow_partial_payment",
"action_on_new_invoice",
"section_break_23",
"item_groups",
"column_break_25",
@@ -424,12 +423,6 @@
"fieldtype": "Select",
"label": "Action on New Invoice",
"options": "Always Ask\nSave Changes and Load New Invoice\nDiscard Changes and Load New Invoice"
},
{
"default": "0",
"fieldname": "allow_partial_payment",
"fieldtype": "Check",
"label": "Allow Partial Payment"
}
],
"grid_page_length": 50,
@@ -458,7 +451,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2025-06-24 11:19:19.834905",
"modified": "2025-05-23 12:12:32.247652",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

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