mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-09 16:32:28 +00:00
Compare commits
1 Commits
v16.0.0
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
741e6a7e52 |
@@ -45,9 +45,3 @@ d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
||||
|
||||
# `frappe.flags.in_test` => `frappe.in_test`
|
||||
7a482a69985c952de0e8193c9d4e086aee65ee6d
|
||||
|
||||
# these commits actually changed something valuable
|
||||
# but they have a lot of whitespace changes that make blame noisy
|
||||
# PR: https://github.com/frappe/erpnext/pull/49816
|
||||
3ffd50c772735877b330d010c1058f623da8721d
|
||||
0e8f8677b8eb31e7834f72d1c6314d3c3f392ca6
|
||||
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout Actions
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: "frappe/backport"
|
||||
path: ./actions
|
||||
|
||||
4
.github/workflows/docs-checker.yml
vendored
4
.github/workflows/docs-checker.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Validate Docs
|
||||
env:
|
||||
|
||||
6
.github/workflows/generate-pot-file.yml
vendored
6
.github/workflows/generate-pot-file.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Run script to update POT file
|
||||
run: |
|
||||
|
||||
2
.github/workflows/initiate_release.yml
vendored
2
.github/workflows/initiate_release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version: ["14", "15", "16"]
|
||||
version: ["14", "15"]
|
||||
|
||||
steps:
|
||||
- uses: octokit/request-action@v2.x
|
||||
|
||||
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
||||
name: linters
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
name: semgrep
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.14
|
||||
uses: actions/setup-python@v6
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
|
||||
- name: Download Semgrep rules
|
||||
|
||||
21
.github/workflows/patch.yml
vendored
21
.github/workflows/patch.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:11.8
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -49,17 +49,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: |
|
||||
3.11
|
||||
3.13
|
||||
3.14
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -135,15 +132,15 @@ jobs:
|
||||
# Resetup env and install apps
|
||||
pgrep honcho | xargs kill
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env --python python$2
|
||||
bench -v setup env
|
||||
bench pip install -e ./apps/erpnext
|
||||
bench start &>> ~/frappe-bench/bench_start.log &
|
||||
|
||||
bench --site test_site migrate
|
||||
}
|
||||
|
||||
update_to_version 14 3.11
|
||||
update_to_version 15 3.13
|
||||
update_to_version 14
|
||||
update_to_version 15
|
||||
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -2,7 +2,7 @@ name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-16
|
||||
- version-13
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,12 +13,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup dependencies
|
||||
|
||||
12
.github/workflows/run-indinvidual-tests.yml
vendored
12
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- id: set-matrix
|
||||
run: |
|
||||
# Use grep and find to get the list of test files
|
||||
@@ -72,17 +72,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
||||
4
.github/workflows/semantic-commits.yml
vendored
4
.github/workflows/semantic-commits.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
name: Check Commit Titles
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 200
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
13
.github/workflows/server-tests-mariadb.yml
vendored
13
.github/workflows/server-tests-mariadb.yml
vendored
@@ -7,7 +7,6 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.svg'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
@@ -63,12 +62,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -79,9 +78,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
@@ -150,7 +149,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
10
.github/workflows/server-tests-postgres.yml
vendored
10
.github/workflows/server-tests-postgres.yml
vendored
@@ -47,12 +47,12 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
@@ -63,9 +63,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.0.0"
|
||||
__version__ = "16.0.0-dev"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -9,20 +9,18 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"last_synced_on": "2026-01-02 13:01:24.037552",
|
||||
"modified": "2026-01-02 13:04:57.850305",
|
||||
"last_synced_on": "2020-07-22 12:19:59.879476",
|
||||
"modified": "2020-07-22 12:21:48.780513",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Balance",
|
||||
"number_of_groups": 0,
|
||||
"owner": "Administrator",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"source": "Account Balance Timeline",
|
||||
"time_interval": "Monthly",
|
||||
"timeseries": 1,
|
||||
"time_interval": "Quarterly",
|
||||
"timeseries": 0,
|
||||
"timespan": "Last Year",
|
||||
"type": "Line",
|
||||
"use_report_chart": 0,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"chart_name": "Profit and Loss",
|
||||
"chart_type": "Report",
|
||||
"creation": "2025-04-01 20:38:16.986176",
|
||||
"creation": "2020-07-17 11:25:34.448572",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dashboard Chart",
|
||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
|
||||
@@ -9,7 +9,7 @@
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2025-12-19 12:37:31.673782",
|
||||
"modified": "2023-07-19 13:08:56.470390",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Profit and Loss",
|
||||
@@ -17,9 +17,8 @@
|
||||
"owner": "Administrator",
|
||||
"report_name": "Profit and Loss Statement",
|
||||
"roles": [],
|
||||
"show_values_over_chart": 1,
|
||||
"timeseries": 0,
|
||||
"type": "Line",
|
||||
"type": "Bar",
|
||||
"use_report_chart": 1,
|
||||
"y_axis": []
|
||||
}
|
||||
}
|
||||
@@ -450,12 +450,14 @@ def process_deferred_accounting(posting_date=None):
|
||||
for company in companies:
|
||||
for record_type in ("Income", "Expense"):
|
||||
doc = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type,
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
company=company.name,
|
||||
posting_date=posting_date,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
type=record_type,
|
||||
)
|
||||
)
|
||||
|
||||
doc.insert()
|
||||
|
||||
@@ -70,7 +70,6 @@ frappe.treeview_settings["Account"] = {
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company,
|
||||
include_default_fb_balances: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -415,13 +415,15 @@ def create_account(**kwargs):
|
||||
return account.name
|
||||
else:
|
||||
account = frappe.get_doc(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
dict(
|
||||
doctype="Account",
|
||||
is_group=kwargs.get("is_group", 0),
|
||||
account_name=kwargs.get("account_name"),
|
||||
account_type=kwargs.get("account_type"),
|
||||
parent_account=kwargs.get("parent_account"),
|
||||
company=kwargs.get("company"),
|
||||
account_currency=kwargs.get("account_currency"),
|
||||
)
|
||||
)
|
||||
|
||||
account.save()
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"exempted_role",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -68,18 +67,10 @@
|
||||
"label": "Closed Documents",
|
||||
"options": "Closed Document",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Role allowed to bypass period restrictions.",
|
||||
"fieldname": "exempted_role",
|
||||
"fieldtype": "Link",
|
||||
"label": "Exempted Role",
|
||||
"link_filters": "[[\"Role\",\"disabled\",\"=\",0]]",
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-12-01 16:53:44.631299",
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
|
||||
@@ -30,7 +30,6 @@ class AccountingPeriod(Document):
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
exempted_role: DF.Link | None
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
# end: auto-generated types
|
||||
@@ -114,7 +113,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
accounting_period = (
|
||||
frappe.qb.from_(ap)
|
||||
.from_(cd)
|
||||
.select(ap.name, ap.exempted_role)
|
||||
.select(ap.name)
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
@@ -127,11 +126,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
).run(as_dict=1)
|
||||
|
||||
if accounting_period:
|
||||
if (
|
||||
accounting_period[0].get("exempted_role")
|
||||
and accounting_period[0].get("exempted_role") in frappe.get_roles()
|
||||
):
|
||||
return
|
||||
frappe.throw(
|
||||
_("You cannot create a {0} within the closed Accounting Period {1}").format(
|
||||
doc.doctype, frappe.bold(accounting_period[0]["name"])
|
||||
|
||||
@@ -37,59 +37,6 @@ class TestAccountingPeriod(IntegrationTestCase):
|
||||
doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.save)
|
||||
|
||||
def test_accounting_period_exempted_role(self):
|
||||
# Create Accounting Period with exempted role
|
||||
ap = create_accounting_period(
|
||||
period_name="Test Accounting Period Exempted",
|
||||
exempted_role="Accounts Manager",
|
||||
start_date="2025-12-01",
|
||||
end_date="2025-12-31",
|
||||
)
|
||||
ap.save()
|
||||
|
||||
# Create users
|
||||
users = frappe.get_all("User", filters={"email": ["like", "test%"]}, limit=1)
|
||||
user = None
|
||||
|
||||
if users[0].name:
|
||||
user = frappe.get_doc("User", users[0].name)
|
||||
else:
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": "test1@example.com",
|
||||
"first_name": "Test1",
|
||||
}
|
||||
)
|
||||
user.insert()
|
||||
|
||||
user.roles = []
|
||||
user.append("roles", {"role": "Accounts User"})
|
||||
|
||||
# ---- Non-exempted user should FAIL ----
|
||||
user.save(ignore_permissions=True)
|
||||
frappe.clear_cache(user=user.name)
|
||||
|
||||
frappe.set_user(user.name)
|
||||
posting_date = "2025-12-11"
|
||||
doc = create_sales_invoice(
|
||||
do_not_save=1,
|
||||
posting_date=posting_date,
|
||||
)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
doc.submit()
|
||||
|
||||
# ---- Exempted role should PASS ----
|
||||
user.append("roles", {"role": "Accounts Manager"})
|
||||
user.save(ignore_permissions=True)
|
||||
frappe.clear_cache(user=user.name)
|
||||
|
||||
doc = create_sales_invoice(do_not_save=1, posting_date=posting_date)
|
||||
|
||||
doc.submit() # Should not raise
|
||||
self.assertEqual(doc.docstatus, 1)
|
||||
|
||||
def tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
frappe.delete_doc("Accounting Period", d.name)
|
||||
@@ -104,6 +51,5 @@ def create_accounting_period(**args):
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
|
||||
accounting_period.exempted_role = args.exempted_role or ""
|
||||
|
||||
return accounting_period
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"column_break_xrnd",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
@@ -75,6 +79,11 @@
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"column_break_25",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
@@ -82,20 +91,13 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"default_ageing_range",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"tab_break_dpet",
|
||||
"chart_of_accounts_section",
|
||||
"show_balance_in_coa",
|
||||
"banking_section",
|
||||
"enable_party_matching",
|
||||
"enable_fuzzy_matching",
|
||||
"payment_request_section",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_section",
|
||||
"budget_settings",
|
||||
"use_legacy_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
@@ -279,6 +281,13 @@
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
@@ -319,6 +328,11 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounts Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_and_billing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -333,6 +347,11 @@
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
@@ -343,7 +362,7 @@
|
||||
{
|
||||
"fieldname": "tab_break_dpet",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Others"
|
||||
"label": "Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -387,6 +406,11 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Auto match and set the Party in Bank Transactions",
|
||||
@@ -462,9 +486,14 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate daily depreciation using total days in depreciation period"
|
||||
},
|
||||
{
|
||||
"description": "Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.",
|
||||
"fieldname": "payment_request_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Payment Requests made from Sales / Purchase Invoice will be put in Draft explicitly",
|
||||
"fieldname": "create_pr_in_draft_status",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create in Draft Status"
|
||||
@@ -506,6 +535,10 @@
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
@@ -545,6 +578,11 @@
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
@@ -611,42 +649,15 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Role to Notify on Depreciation Failure",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "30, 60, 90, 120",
|
||||
"fieldname": "default_ageing_range",
|
||||
"fieldtype": "Data",
|
||||
"label": "Default Ageing Range"
|
||||
},
|
||||
{
|
||||
"fieldname": "chart_of_accounts_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Chart Of Accounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "banking_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Banking"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_request_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Request"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Budget"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-11 18:30:45.968531",
|
||||
"modified": "2025-12-03 20:42:13.238050",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -40,7 +40,6 @@ class AccountsSettings(Document):
|
||||
confirm_before_resetting_posting_date: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_common_party_accounting: DF.Check
|
||||
@@ -57,6 +56,7 @@ class AccountsSettings(Document):
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
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_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
@@ -152,5 +152,6 @@ class AccountsSettings(Document):
|
||||
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}")
|
||||
|
||||
57
erpnext/accounts/doctype/advance_tax/advance_tax.json
Normal file
57
erpnext/accounts/doctype/advance_tax/advance_tax.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-11-25 10:24:39.836195",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"reference_detail",
|
||||
"account_head",
|
||||
"allocated_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Reference Name",
|
||||
"options": "reference_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_detail",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Head",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "party_account_currency"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:58.308002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Tax",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
27
erpnext/accounts/doctype/advance_tax/advance_tax.py
Normal file
27
erpnext/accounts/doctype/advance_tax/advance_tax.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AdvanceTax(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
account_head: DF.Link | None
|
||||
allocated_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
reference_detail: DF.Data | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
reference_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -14,7 +14,6 @@
|
||||
"description",
|
||||
"included_in_paid_amount",
|
||||
"set_by_item_tax_template",
|
||||
"is_tax_withholding_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -26,6 +25,7 @@
|
||||
"net_amount",
|
||||
"tax_amount",
|
||||
"total",
|
||||
"allocated_amount",
|
||||
"column_break_13",
|
||||
"base_tax_amount",
|
||||
"base_net_amount",
|
||||
@@ -97,11 +97,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
@@ -172,6 +172,12 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Considered In Paid Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fetch_from": "account_head.account_currency",
|
||||
"fieldname": "currency",
|
||||
@@ -207,26 +213,18 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-15 06:42:18.707671",
|
||||
"modified": "2024-11-22 19:16:22.346267",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class AdvanceTaxesandCharges(Document):
|
||||
|
||||
account_head: DF.Link
|
||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||
allocated_amount: DF.Currency
|
||||
base_net_amount: DF.Currency
|
||||
base_tax_amount: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -27,12 +28,10 @@ class AdvanceTaxesandCharges(Document):
|
||||
currency: DF.Link | None
|
||||
description: DF.SmallText
|
||||
included_in_paid_amount: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
net_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
set_by_item_tax_template: DF.Check
|
||||
|
||||
@@ -125,7 +125,7 @@ class BankClearance(Document):
|
||||
)
|
||||
|
||||
msg += "</ul>"
|
||||
msgprint(_(msg))
|
||||
frappe.throw(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
@@ -134,44 +134,16 @@ class BankClearance(Document):
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
old_clearance_date = frappe.db.get_value(
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{
|
||||
"parent": d.payment_entry,
|
||||
"account": self.account,
|
||||
"amount": [">", 0],
|
||||
},
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
if d.clearance_date or old_clearance_date:
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
sales_invoice = frappe.get_lazy_doc("Sales Invoice", d.payment_entry)
|
||||
sales_invoice.add_comment(
|
||||
"Comment",
|
||||
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
|
||||
old_clearance_date, d.clearance_date
|
||||
),
|
||||
)
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
old_clearance_date = payment_entry.clearance_date
|
||||
|
||||
if d.clearance_date or old_clearance_date:
|
||||
# using db_set to trigger notification
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
payment_entry.add_comment(
|
||||
"Comment",
|
||||
_("Clearance date changed from {0} to {1} via Bank Clearance Tool").format(
|
||||
old_clearance_date, d.clearance_date
|
||||
),
|
||||
)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"label": "Payment Entry",
|
||||
"oldfieldname": "voucher_id",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "payment_document"
|
||||
"options": "payment_document",
|
||||
"width": "50"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
@@ -68,7 +69,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "cheque_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@@ -78,10 +79,8 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "cheque_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Cheque Date",
|
||||
"oldfieldname": "cheque_date",
|
||||
"oldfieldtype": "Date",
|
||||
@@ -97,19 +96,17 @@
|
||||
"oldfieldtype": "Date"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-17 14:33:45.913311",
|
||||
"modified": "2024-03-27 13:06:37.609319",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Clearance Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,6 @@ def create_payment_entry_bts(
|
||||
project=None,
|
||||
cost_center=None,
|
||||
allow_edit=None,
|
||||
company_bank_account=None,
|
||||
):
|
||||
# Create a new payment entry based on the bank transaction
|
||||
bank_transaction = frappe.db.get_values(
|
||||
@@ -346,9 +345,6 @@ def create_payment_entry_bts(
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
if company_bank_account:
|
||||
pe.bank_account = company_bank_account
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
|
||||
@@ -38,10 +38,7 @@
|
||||
"column_break_3czf",
|
||||
"bank_party_name",
|
||||
"bank_party_account_number",
|
||||
"bank_party_iban",
|
||||
"extended_bank_statement_section",
|
||||
"included_fee",
|
||||
"excluded_fee"
|
||||
"bank_party_iban"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -236,32 +233,12 @@
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "extended_bank_statement_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Extended Bank Statement"
|
||||
},
|
||||
{
|
||||
"fieldname": "included_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Included Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
|
||||
"fieldname": "excluded_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Excluded Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-07 20:49:18.600757",
|
||||
"modified": "2025-10-23 17:32:58.514807",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -32,8 +32,6 @@ class BankTransaction(Document):
|
||||
date: DF.Date | None
|
||||
deposit: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
excluded_fee: DF.Currency
|
||||
included_fee: DF.Currency
|
||||
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
@@ -47,14 +45,9 @@ class BankTransaction(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.handle_excluded_fee()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def validate(self):
|
||||
self.validate_included_fee()
|
||||
self.validate_duplicate_references()
|
||||
self.validate_currency()
|
||||
|
||||
@@ -314,40 +307,6 @@ class BankTransaction(Document):
|
||||
|
||||
self.party_type, self.party = result
|
||||
|
||||
def validate_included_fee(self):
|
||||
"""
|
||||
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
|
||||
therefore outside of the deposit value and can be larger than the deposit itself.
|
||||
"""
|
||||
|
||||
if self.included_fee and self.withdrawal:
|
||||
if self.included_fee > self.withdrawal:
|
||||
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
|
||||
|
||||
def handle_excluded_fee(self):
|
||||
# Include the excluded fee on validate to handle all further processing the same
|
||||
excluded_fee = flt(self.excluded_fee)
|
||||
if excluded_fee <= 0:
|
||||
return
|
||||
|
||||
# Suppress a negative deposit (aka withdrawal), likely not intendend
|
||||
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
|
||||
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
|
||||
|
||||
# Enforce directionality
|
||||
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
|
||||
frappe.throw(
|
||||
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
|
||||
)
|
||||
|
||||
if flt(self.deposit) > 0:
|
||||
self.deposit = flt(self.deposit) - excluded_fee
|
||||
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
|
||||
elif flt(self.withdrawal) >= 0:
|
||||
self.withdrawal = flt(self.withdrawal) + excluded_fee
|
||||
self.included_fee = flt(self.included_fee) + excluded_fee
|
||||
self.excluded_fee = 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
|
||||
|
||||
class TestBankTransactionFees(UnitTestCase):
|
||||
def test_included_fee_throws(self):
|
||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||
withdrawal itself."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 101
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
|
||||
|
||||
def test_included_fee_allows_equal(self):
|
||||
"""A fee that's part of a withdrawal may be equal to the withdrawal
|
||||
amount (only the fee was deducted from the account)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 100
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_included_fee_allows_for_deposit(self):
|
||||
"""For deposits, a fee may be recorded separately without limiting the
|
||||
received amount."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.included_fee = 999
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_excluded_fee_noop_when_zero(self):
|
||||
"""When there is no excluded fee to apply, the amounts should remain
|
||||
unchanged."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 5
|
||||
bt.excluded_fee = 0
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 100)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_throws_when_exceeds_deposit(self):
|
||||
"""A fee deducted from an incoming payment must not exceed the incoming
|
||||
amount (else it would be a withdrawal, a conversion we don't support)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.excluded_fee = 11
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
|
||||
"""A transaction must be either incoming or outgoing when applying a
|
||||
fee, not both."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.withdrawal = 10
|
||||
bt.excluded_fee = 1
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_deducts_from_deposit(self):
|
||||
"""When a fee is deducted from an incoming payment, the net received
|
||||
amount decreases and the fee is tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 95)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
|
||||
"""A separately-deducted fee may reduce an incoming payment to zero,
|
||||
while still tracking the fee."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 5
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_increases_outgoing_payment(self):
|
||||
"""When a separately-deducted fee is provided for an outgoing payment,
|
||||
the total money leaving increases and the fee is tracked."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 105)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
|
||||
"""If only an excluded fee is provided, it should be treated as an
|
||||
outgoing payment and the fee is then tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 5)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
@@ -12,15 +12,6 @@ frappe.ui.form.on("Budget", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
@@ -33,16 +24,24 @@ frappe.ui.form.on("Budget", {
|
||||
frm.trigger("toggle_reqd_fields");
|
||||
|
||||
if (!frm.doc.__islocal && frm.doc.docstatus == 1) {
|
||||
frm.add_custom_button(
|
||||
__("Revise Budget"),
|
||||
function () {
|
||||
frm.events.revise_budget_action(frm);
|
||||
},
|
||||
__("Actions")
|
||||
let exception_role = await frappe.db.get_value(
|
||||
"Company",
|
||||
frm.doc.company,
|
||||
"exception_budget_approver_role"
|
||||
);
|
||||
}
|
||||
|
||||
toggle_distribution_fields(frm);
|
||||
const role = exception_role.message.exception_budget_approver_role;
|
||||
|
||||
if (role && frappe.user.has_role(role)) {
|
||||
frm.add_custom_button(
|
||||
__("Revise Budget"),
|
||||
function () {
|
||||
frm.events.revise_budget_action(frm);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
budget_against: function (frm) {
|
||||
@@ -55,15 +54,10 @@ frappe.ui.form.on("Budget", {
|
||||
frm.doc.budget_distribution.forEach((row) => {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
});
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
|
||||
distribute_equally: function (frm) {
|
||||
toggle_distribution_fields(frm);
|
||||
},
|
||||
|
||||
set_null_value: function (frm) {
|
||||
if (frm.doc.budget_against == "Cost Center") {
|
||||
frm.set_value("project", null);
|
||||
@@ -106,8 +100,6 @@ frappe.ui.form.on("Budget Distribution", {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.percent = flt((row.amount / frm.doc.budget_amount) * 100, 2);
|
||||
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
@@ -115,29 +107,7 @@ frappe.ui.form.on("Budget Distribution", {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
if (frm.doc.budget_amount) {
|
||||
row.amount = flt((row.percent / 100) * frm.doc.budget_amount, 2);
|
||||
|
||||
set_total_budget_amount(frm);
|
||||
frm.refresh_field("budget_distribution");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function set_total_budget_amount(frm) {
|
||||
let total = 0;
|
||||
|
||||
(frm.doc.budget_distribution || []).forEach((row) => {
|
||||
total += flt(row.amount);
|
||||
});
|
||||
|
||||
frm.set_value("budget_distribution_total", total);
|
||||
}
|
||||
|
||||
function toggle_distribution_fields(frm) {
|
||||
const grid = frm.fields_dict.budget_distribution.grid;
|
||||
|
||||
["amount", "percent"].forEach((field) => {
|
||||
grid.update_docfield_property(field, "read_only", frm.doc.distribute_equally);
|
||||
});
|
||||
|
||||
grid.refresh();
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
"distribute_equally",
|
||||
"section_break_fpdt",
|
||||
"budget_distribution",
|
||||
"section_break_wkqb",
|
||||
"column_break_paum",
|
||||
"column_break_nwor",
|
||||
"budget_distribution_total",
|
||||
"section_break_6",
|
||||
"applicable_on_material_request",
|
||||
"action_if_annual_budget_exceeded_on_mr",
|
||||
@@ -226,8 +222,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fpdt",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_distribution",
|
||||
@@ -308,32 +303,13 @@
|
||||
"options": "Monthly\nQuarterly\nHalf-Yearly\nYearly",
|
||||
"read_only_depends_on": "eval: doc.revision_of",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_wkqb",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_paum",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_nwor",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_distribution_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Budget Distribution Total",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-10 02:35:01.197613",
|
||||
"modified": "2025-11-19 17:00:00.648224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
|
||||
@@ -53,7 +53,6 @@ class Budget(Document):
|
||||
budget_against: DF.Literal["", "Cost Center", "Project"]
|
||||
budget_amount: DF.Currency
|
||||
budget_distribution: DF.Table[BudgetDistribution]
|
||||
budget_distribution_total: DF.Currency
|
||||
budget_end_date: DF.Date | None
|
||||
budget_start_date: DF.Date | None
|
||||
company: DF.Link
|
||||
@@ -231,49 +230,28 @@ class Budget(Document):
|
||||
|
||||
def before_save(self):
|
||||
self.allocate_budget()
|
||||
self.budget_distribution_total = sum(flt(row.amount) for row in self.budget_distribution)
|
||||
|
||||
def on_update(self):
|
||||
self.validate_distribution_totals()
|
||||
|
||||
def allocate_budget(self):
|
||||
if self._should_skip_allocation():
|
||||
return
|
||||
|
||||
if self._should_recalculate_manual_distribution():
|
||||
self._recalculate_manual_distribution()
|
||||
if self.revision_of:
|
||||
return
|
||||
|
||||
if not self.should_regenerate_budget_distribution():
|
||||
return
|
||||
|
||||
self._regenerate_distribution()
|
||||
self.set("budget_distribution", [])
|
||||
|
||||
def _should_skip_allocation(self):
|
||||
return self.revision_of and not self.distribute_equally
|
||||
periods = self.get_budget_periods()
|
||||
total_periods = len(periods)
|
||||
row_percent = 100 / total_periods if total_periods else 0
|
||||
|
||||
def _should_recalculate_manual_distribution(self):
|
||||
return (
|
||||
not self.distribute_equally
|
||||
and bool(self.budget_distribution)
|
||||
and self._is_only_budget_amount_changed()
|
||||
)
|
||||
|
||||
def _is_only_budget_amount_changed(self):
|
||||
old = self.get_doc_before_save()
|
||||
if not old:
|
||||
return False
|
||||
|
||||
return (
|
||||
old.budget_amount != self.budget_amount
|
||||
and old.distribution_frequency == self.distribution_frequency
|
||||
and old.budget_start_date == self.budget_start_date
|
||||
and old.budget_end_date == self.budget_end_date
|
||||
)
|
||||
|
||||
def _recalculate_manual_distribution(self):
|
||||
for row in self.budget_distribution:
|
||||
row.amount = flt((row.percent / 100) * self.budget_amount, 3)
|
||||
for start_date, end_date in periods:
|
||||
row = self.append("budget_distribution", {})
|
||||
row.start_date = start_date
|
||||
row.end_date = end_date
|
||||
self.add_allocated_amount(row, row_percent)
|
||||
|
||||
def should_regenerate_budget_distribution(self):
|
||||
"""Check whether budget distribution should be recalculated."""
|
||||
@@ -287,6 +265,7 @@ class Budget(Document):
|
||||
"to_fiscal_year",
|
||||
"budget_amount",
|
||||
"distribution_frequency",
|
||||
"distribute_equally",
|
||||
]
|
||||
for field in changed_fields:
|
||||
if old_doc.get(field) != self.get(field):
|
||||
@@ -294,21 +273,6 @@ class Budget(Document):
|
||||
|
||||
return bool(self.distribute_equally)
|
||||
|
||||
def _regenerate_distribution(self):
|
||||
self.set("budget_distribution", [])
|
||||
|
||||
periods = self.get_budget_periods()
|
||||
total_periods = len(periods)
|
||||
row_percent = 100 / total_periods if total_periods else 0
|
||||
|
||||
for start_date, end_date in periods:
|
||||
row = self.append("budget_distribution", {})
|
||||
row.start_date = start_date
|
||||
row.end_date = end_date
|
||||
self.add_allocated_amount(row, row_percent)
|
||||
|
||||
self.budget_distribution_total = self.budget_amount
|
||||
|
||||
def get_budget_periods(self):
|
||||
"""Return list of (start_date, end_date) tuples based on frequency."""
|
||||
frequency = self.distribution_frequency
|
||||
@@ -348,8 +312,12 @@ class Budget(Document):
|
||||
}.get(frequency, 1)
|
||||
|
||||
def add_allocated_amount(self, row, row_percent):
|
||||
row.amount = flt(self.budget_amount * row_percent / 100, 3)
|
||||
row.percent = flt(row_percent, 3)
|
||||
if not self.distribute_equally:
|
||||
row.amount = 0
|
||||
row.percent = 0
|
||||
else:
|
||||
row.amount = flt(self.budget_amount * row_percent / 100, 3)
|
||||
row.percent = flt(row_percent, 3)
|
||||
|
||||
def validate_distribution_totals(self):
|
||||
if self.should_regenerate_budget_distribution():
|
||||
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
} else if (frm.doc.service_provider == "frankfurter.dev") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
@@ -101,11 +101,10 @@
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:19:02.873815",
|
||||
"modified": "2025-11-25 13:03:41.896424",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
|
||||
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
|
||||
elif self.service_provider == "frankfurter.dev":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
@@ -105,11 +105,9 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
|
||||
|
||||
@@ -71,9 +71,7 @@ class PeriodValue:
|
||||
class AccountData:
|
||||
"""Account data across all periods"""
|
||||
|
||||
account: str # docname
|
||||
account_name: str = "" # account name
|
||||
account_number: str = ""
|
||||
account_name: str
|
||||
period_values: dict[str, PeriodValue] = field(default_factory=dict)
|
||||
|
||||
def add_period(self, period_value: PeriodValue) -> None:
|
||||
@@ -105,11 +103,7 @@ class AccountData:
|
||||
# movement is unaccumulated by default
|
||||
|
||||
def copy(self):
|
||||
copied = AccountData(
|
||||
account=self.account,
|
||||
account_name=self.account_name,
|
||||
account_number=self.account_number,
|
||||
)
|
||||
copied = AccountData(account_name=self.account_name)
|
||||
copied.period_values = {k: v.copy() for k, v in self.period_values.items()}
|
||||
return copied
|
||||
|
||||
@@ -335,10 +329,12 @@ class DataCollector:
|
||||
self.account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
|
||||
|
||||
def add_account_request(self, row):
|
||||
accounts = self._parse_account_filter(self.company, row)
|
||||
|
||||
self.account_requests.append(
|
||||
{
|
||||
"row": row,
|
||||
"accounts": self._parse_account_filter(self.company, row),
|
||||
"accounts": accounts,
|
||||
"balance_type": row.balance_type,
|
||||
"reference_code": row.reference_code,
|
||||
"reverse_sign": row.reverse_sign,
|
||||
@@ -349,12 +345,12 @@ class DataCollector:
|
||||
if not self.account_requests:
|
||||
return {"account_data": {}, "summary": {}, "account_details": {}}
|
||||
|
||||
# Get all accounts
|
||||
all_accounts = []
|
||||
|
||||
# Get all unique accounts
|
||||
all_accounts = set()
|
||||
for request in self.account_requests:
|
||||
all_accounts.extend(request["accounts"])
|
||||
all_accounts.update(request["accounts"])
|
||||
|
||||
all_accounts = list(all_accounts)
|
||||
if not all_accounts:
|
||||
return {"account_data": {}, "summary": {}, "account_details": {}}
|
||||
|
||||
@@ -377,9 +373,7 @@ class DataCollector:
|
||||
total_values = [0.0] * len(self.periods)
|
||||
request_account_details = {}
|
||||
|
||||
for account in accounts:
|
||||
account_name = account.name
|
||||
|
||||
for account_name in accounts:
|
||||
if account_name not in account_data:
|
||||
continue
|
||||
|
||||
@@ -402,21 +396,20 @@ class DataCollector:
|
||||
return {"account_data": account_data, "summary": summary, "account_details": account_details}
|
||||
|
||||
@staticmethod
|
||||
def _parse_account_filter(company, report_row) -> list[dict]:
|
||||
def _parse_account_filter(company, report_row) -> list[str]:
|
||||
"""
|
||||
Find accounts matching filter criteria.
|
||||
|
||||
Example:
|
||||
|
||||
- Input: '["account_type", "=", "Cash"]'
|
||||
- Output: [{"name": "Cash - COMP", "account_name": "Cash", "account_number": "1001"}]
|
||||
Input: '["account_type", "=", "Cash"]'
|
||||
Output: ["Cash - COMP", "Petty Cash - COMP", "Bank - COMP"]
|
||||
"""
|
||||
filter_parser = FilterExpressionParser()
|
||||
|
||||
account = frappe.qb.DocType("Account")
|
||||
query = (
|
||||
frappe.qb.from_(account)
|
||||
.select(account.name, account.account_name, account.account_number)
|
||||
.select(account.name)
|
||||
.where(account.disabled == 0)
|
||||
.where(account.is_group == 0)
|
||||
)
|
||||
@@ -430,8 +423,8 @@ class DataCollector:
|
||||
|
||||
query = query.where(where_condition)
|
||||
query = query.orderby(account.name)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
result = query.run(as_dict=True)
|
||||
return [row.name for row in result]
|
||||
|
||||
@staticmethod
|
||||
def get_filtered_accounts(company: str, account_rows: list) -> list[str]:
|
||||
@@ -463,35 +456,17 @@ class FinancialQueryBuilder:
|
||||
self.filters = filters
|
||||
self.periods = periods
|
||||
self.company = filters.get("company")
|
||||
self.account_meta = {} # {name: {account_name, account_number}}
|
||||
|
||||
def fetch_account_balances(self, accounts: list[dict]) -> dict[str, AccountData]:
|
||||
def fetch_account_balances(self, accounts: list[str]) -> dict[str, AccountData]:
|
||||
"""
|
||||
Fetch account balances for all periods with optimization.
|
||||
Steps: get opening balances → fetch GL entries → calculate running totals
|
||||
|
||||
- accounts: list of accounts with details
|
||||
|
||||
```
|
||||
{
|
||||
"name": "Cash - COMP",
|
||||
"account_name": "Cash",
|
||||
"account_number": "1001",
|
||||
}
|
||||
```
|
||||
|
||||
Returns:
|
||||
dict: {account: AccountData}
|
||||
"""
|
||||
account_names = list({acc.name for acc in accounts})
|
||||
# NOTE: do not change accounts list as it is used in caller function
|
||||
self.account_meta = {
|
||||
acc.name: {"account_name": acc.account_name, "account_number": acc.account_number}
|
||||
for acc in accounts
|
||||
}
|
||||
|
||||
balances_data = self._get_opening_balances(account_names)
|
||||
gl_data = self._get_gl_movements(account_names)
|
||||
balances_data = self._get_opening_balances(accounts)
|
||||
gl_data = self._get_gl_movements(accounts)
|
||||
self._calculate_running_balances(balances_data, gl_data)
|
||||
self._handle_balance_accumulation(balances_data)
|
||||
|
||||
@@ -568,8 +543,7 @@ class FinancialQueryBuilder:
|
||||
gap_movement = gap_movements.get(account, 0.0)
|
||||
opening_balance = closing_balance + gap_movement
|
||||
|
||||
account_data = AccountData(account=account, **self._get_account_meta(account))
|
||||
|
||||
account_data = AccountData(account)
|
||||
account_data.add_period(PeriodValue(first_period_key, opening_balance, 0, 0))
|
||||
balances_data[account] = account_data
|
||||
|
||||
@@ -639,7 +613,7 @@ class FinancialQueryBuilder:
|
||||
for row in gl_data:
|
||||
account = row["account"]
|
||||
if account not in balances_data:
|
||||
balances_data[account] = AccountData(account=account, **self._get_account_meta(account))
|
||||
balances_data[account] = AccountData(account)
|
||||
|
||||
account_data: AccountData = balances_data[account]
|
||||
|
||||
@@ -740,9 +714,6 @@ class FinancialQueryBuilder:
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def _get_account_meta(self, account: str) -> dict[str, Any]:
|
||||
return self.account_meta.get(account, {})
|
||||
|
||||
|
||||
class FilterExpressionParser:
|
||||
"""Direct filter expression to SQL condition builder"""
|
||||
@@ -1573,29 +1544,20 @@ class RowFormatterBase(ABC):
|
||||
pass
|
||||
|
||||
def _get_values(self, row_data: RowData) -> dict[str, Any]:
|
||||
def _get_row_data(key: str, default: Any = "") -> Any:
|
||||
return getattr(row_data.row, key, default) or default
|
||||
|
||||
def _get_filter_value(key: str, default: Any = "") -> Any:
|
||||
return getattr(self.context.filters, key, default) or default
|
||||
|
||||
# TODO: can be commonify COA? @abdeali
|
||||
child_accounts = []
|
||||
|
||||
if row_data.account_details:
|
||||
child_accounts = list(row_data.account_details.keys())
|
||||
|
||||
display_name = _get_row_data("display_name", "")
|
||||
|
||||
values = {
|
||||
"account": _get_row_data("account", "") or display_name,
|
||||
"account_name": display_name,
|
||||
"acc_name": _get_row_data("account_name", ""),
|
||||
"acc_number": _get_row_data("account_number", ""),
|
||||
"child_accounts": child_accounts,
|
||||
"account": getattr(row_data.row, "display_name", "") or "",
|
||||
"indent": getattr(row_data.row, "indentation_level", 0),
|
||||
"account_name": getattr(row_data.row, "account", "") or "",
|
||||
"currency": self.context.currency or "",
|
||||
"indent": _get_row_data("indentation_level", 0),
|
||||
"period_start_date": _get_filter_value("period_start_date", ""),
|
||||
"period_end_date": _get_filter_value("period_end_date", ""),
|
||||
"period_start_date": getattr(self.context.filters, "period_start_date", "") or "",
|
||||
"period_end_date": getattr(self.context.filters, "period_end_date", "") or "",
|
||||
"total": 0,
|
||||
}
|
||||
|
||||
@@ -1708,8 +1670,8 @@ class DetailRowBuilder:
|
||||
detail_rows = []
|
||||
parent_row = self.parent_row_data.row
|
||||
|
||||
for account_data in self.parent_row_data.account_details.values():
|
||||
detail_row = self._create_detail_row_object(account_data, parent_row)
|
||||
for account_name, account_data in self.parent_row_data.account_details.items():
|
||||
detail_row = self._create_detail_row_object(account_name, parent_row)
|
||||
|
||||
balance_type = getattr(parent_row, "balance_type", "Closing Balance")
|
||||
values = account_data.get_values_by_type(balance_type)
|
||||
@@ -1725,20 +1687,16 @@ class DetailRowBuilder:
|
||||
|
||||
return detail_rows
|
||||
|
||||
def _create_detail_row_object(self, account_data: AccountData, parent_row):
|
||||
acc_name = account_data.account_name or ""
|
||||
acc_number = account_data.account_number or ""
|
||||
|
||||
display_name = f"{_(acc_number)} - {_(acc_name)}" if acc_number else _(acc_name)
|
||||
def _create_detail_row_object(self, account_name: str, parent_row):
|
||||
short_name = account_name.rsplit(" - ", 1)[0].strip()
|
||||
|
||||
return type(
|
||||
"DetailRow",
|
||||
(),
|
||||
{
|
||||
"account": account_data.account,
|
||||
"display_name": display_name,
|
||||
"account_name": acc_name,
|
||||
"account_number": acc_number,
|
||||
"display_name": short_name,
|
||||
"account": account_name,
|
||||
"account_name": short_name,
|
||||
"data_source": "Account Detail",
|
||||
"indentation_level": getattr(parent_row, "indentation_level", 0) + 1,
|
||||
"fieldtype": getattr(parent_row, "fieldtype", None),
|
||||
|
||||
@@ -4,7 +4,6 @@ from frappe import _
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "fiscal_year",
|
||||
"non_standard_fieldnames": {"Budget": "from_fiscal_year"},
|
||||
"transactions": [
|
||||
{"label": _("Budgets"), "items": ["Budget"]},
|
||||
{"label": _("References"), "items": ["Period Closing Voucher"]},
|
||||
|
||||
@@ -193,6 +193,7 @@ class GLEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -206,6 +207,7 @@ class GLEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
|
||||
@@ -9,8 +9,8 @@ frappe.listview_settings["Invoice Discounting"] = {
|
||||
return [__("Disbursed"), "blue", "status,=,Disbursed"];
|
||||
} else if (doc.status == "Settled") {
|
||||
return [__("Settled"), "orange", "status,=,Settled"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
} else if (doc.status == "Canceled") {
|
||||
return [__("Canceled"), "red", "status,=,Canceled"];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,20 +43,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (row.party_type == "Customer") {
|
||||
filters.customer = row.party;
|
||||
}
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
get_balance_for_periodic_accounting(frm) {
|
||||
@@ -126,11 +112,9 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
|
||||
if (frm.doc.voucher_type !== "Exchange Gain Or Loss") {
|
||||
$.each(frm.doc.accounts || [], function (i, row) {
|
||||
erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name);
|
||||
});
|
||||
}
|
||||
$.each(frm.doc.accounts || [], function (i, row) {
|
||||
erpnext.journal_entry.set_exchange_rate(frm, row.doctype, row.name);
|
||||
});
|
||||
},
|
||||
before_save: function (frm) {
|
||||
if (frm.doc.docstatus == 0 && !frm.doc.is_system_generated) {
|
||||
@@ -217,7 +201,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
@@ -268,10 +251,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function (frm) {
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
});
|
||||
|
||||
var update_jv_details = function (doc, r) {
|
||||
|
||||
@@ -43,11 +43,6 @@
|
||||
"total_amount_currency",
|
||||
"total_amount",
|
||||
"total_amount_in_words",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
@@ -522,7 +517,7 @@
|
||||
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding "
|
||||
"label": "Apply Tax Withholding Amount "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus",
|
||||
@@ -591,39 +586,6 @@
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -638,7 +600,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-13 17:54:14.542903",
|
||||
"modified": "2025-09-29 13:05:46.982277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -6,7 +6,6 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
|
||||
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
|
||||
import erpnext
|
||||
@@ -18,7 +17,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import JournalTaxWithholding
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
@@ -32,7 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -49,7 +49,6 @@ class JournalEntry(AccountsController):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry_account.journal_entry_account import JournalEntryAccount
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
|
||||
accounts: DF.Table[JournalEntryAccount]
|
||||
amended_from: DF.Link | None
|
||||
@@ -66,7 +65,6 @@ class JournalEntry(AccountsController):
|
||||
finance_book: DF.Link | None
|
||||
for_all_stock_asset_accounts: DF.Check
|
||||
from_template: DF.Link | None
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
inter_company_journal_entry_reference: DF.Link | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
is_system_generated: DF.Check
|
||||
@@ -75,7 +73,6 @@ class JournalEntry(AccountsController):
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
override_tax_withholding_entries: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -87,8 +84,6 @@ class JournalEntry(AccountsController):
|
||||
stock_asset_account: DF.Link | None
|
||||
stock_entry: DF.Link | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
title: DF.Data | None
|
||||
total_amount: DF.Currency
|
||||
total_amount_currency: DF.Link | None
|
||||
@@ -155,8 +150,8 @@ class JournalEntry(AccountsController):
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
JournalTaxWithholding(self).on_validate()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
@@ -180,13 +175,15 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_submit")
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("submit", timeout=4600)
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_cancel")
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("cancel", timeout=4600)
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
@@ -202,7 +199,6 @@ class JournalEntry(AccountsController):
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
JournalTaxWithholding(self).on_submit()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_for_periodic_accounting(self):
|
||||
@@ -286,8 +282,6 @@ class JournalEntry(AccountsController):
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# Cancel tax withholding entries
|
||||
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = (
|
||||
@@ -301,10 +295,8 @@ class JournalEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
JournalTaxWithholding(self).on_cancel()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
@@ -360,6 +352,95 @@ class JournalEntry(AccountsController):
|
||||
StockAccountInvalidTransaction,
|
||||
)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get("accounts") if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
||||
debit_or_credit = (
|
||||
"debit_in_account_currency"
|
||||
if self.voucher_type == "Credit Note"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
rev_debit_or_credit = (
|
||||
"credit_in_account_currency"
|
||||
if debit_or_credit == "debit_in_account_currency"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(
|
||||
d.get(debit_or_credit)
|
||||
for d in self.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
)
|
||||
|
||||
party_amount = sum(
|
||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
||||
)
|
||||
|
||||
inv = frappe._dict(
|
||||
{
|
||||
party_type: parties[0],
|
||||
"doctype": doctype,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get("accounts"):
|
||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
||||
d.update(
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(d.get("account"))
|
||||
|
||||
if d.get("account") == party_account:
|
||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
"against_account": parties[0],
|
||||
},
|
||||
)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.get("accounts")
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
self.update_asset_on_depreciation()
|
||||
self.update_asset_on_disposal()
|
||||
@@ -1673,6 +1754,8 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
@@ -1695,7 +1778,7 @@ def get_exchange_rate(
|
||||
# The date used to retreive the exchange rate here is the date passed
|
||||
# in as an argument to this function.
|
||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"is_tax_withholding_account",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -282,19 +281,12 @@
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified": "2025-10-27 13:48:32.805100",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,7 +28,6 @@ class JournalEntryAccount(Document):
|
||||
debit_in_account_currency: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
is_advance: DF.Literal["No", "Yes"]
|
||||
is_tax_withholding_account: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
|
||||
["Account", "account_type", "in", "Bank, Cash, Receivable"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", d.company],
|
||||
],
|
||||
|
||||
@@ -11,5 +11,6 @@ def get_data():
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
|
||||
{"items": ["Budget"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -214,9 +214,6 @@ class OpeningInvoiceCreationTool(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if self.invoice_type == "Purchase" and row.supplier_invoice_date:
|
||||
invoice.update({"bill_date": row.supplier_invoice_date})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
for dimension in accounting_dimension:
|
||||
invoice.update({dimension: self.get(dimension) or item.get(dimension)})
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"column_break_3",
|
||||
"posting_date",
|
||||
"due_date",
|
||||
"supplier_invoice_date",
|
||||
"section_break_5",
|
||||
"item_name",
|
||||
"outstanding_amount",
|
||||
@@ -112,26 +111,19 @@
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Invoice Number"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: parent.invoice_type == \"Purchase\"",
|
||||
"fieldname": "supplier_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Supplier Invoice Date"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-01 16:18:07.997594",
|
||||
"modified": "2024-03-27 13:10:06.703006",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ class OpeningInvoiceCreationToolItem(Document):
|
||||
party_type: DF.Link | None
|
||||
posting_date: DF.Date | None
|
||||
qty: DF.Data | None
|
||||
supplier_invoice_date: DF.Date | None
|
||||
temporary_opening_account: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -182,7 +181,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"Dunning",
|
||||
];
|
||||
|
||||
if (party_type_doctypes.includes(child.reference_doctype)) {
|
||||
if (in_list(party_type_doctypes, child.reference_doctype)) {
|
||||
filters[doc.party_type.toLowerCase()] = doc.party;
|
||||
}
|
||||
|
||||
@@ -427,15 +426,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
[
|
||||
"party",
|
||||
"party_type",
|
||||
"paid_from",
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
"party_name",
|
||||
],
|
||||
["party", "party_type", "paid_from", "paid_to", "references", "total_allocated_amount"],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
}
|
||||
@@ -541,7 +532,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
() => frm.set_value("party_name", r.message.party_name),
|
||||
() => frm.clear_table("references"),
|
||||
() => frm.clear_table("tax_withholding_entries"),
|
||||
() => frm.events.hide_unhide_fields(frm),
|
||||
() => frm.events.set_dynamic_labels(frm),
|
||||
() => {
|
||||
@@ -574,15 +564,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
},
|
||||
|
||||
apply_tds: function (frm) {
|
||||
if (!frm.doc.apply_tds) {
|
||||
apply_tax_withholding_amount: function (frm) {
|
||||
if (!frm.doc.apply_tax_withholding_amount) {
|
||||
frm.set_value("tax_withholding_category", "");
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
frappe.db.get_value(frm.doc.party_type, frm.doc.party, "tax_withholding_category", (values) => {
|
||||
} else {
|
||||
frappe.db.get_value("Supplier", frm.doc.party, "tax_withholding_category", (values) => {
|
||||
frm.set_value("tax_withholding_category", values.tax_withholding_category);
|
||||
});
|
||||
}
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
},
|
||||
|
||||
paid_from: function (frm) {
|
||||
@@ -1041,7 +1030,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
c.allocated_amount = d.allocated_amount;
|
||||
c.account = d.account;
|
||||
|
||||
if (!frm.events.get_order_doctypes(frm).includes(d.voucher_type)) {
|
||||
if (!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) {
|
||||
if (flt(d.outstanding_amount) > 0)
|
||||
total_positive_outstanding += flt(d.outstanding_amount);
|
||||
else total_negative_outstanding += Math.abs(flt(d.outstanding_amount));
|
||||
@@ -1057,7 +1046,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
} else {
|
||||
c.exchange_rate = 1;
|
||||
}
|
||||
if (frm.events.get_invoice_doctypes(frm).includes(d.reference_doctype)) {
|
||||
if (in_list(frm.events.get_invoice_doctypes(frm), d.reference_doctype)) {
|
||||
c.due_date = d.due_date;
|
||||
}
|
||||
});
|
||||
@@ -1288,14 +1277,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const account =
|
||||
company_defaults?.[account_fieldname] ||
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = company_defaults?.cost_center;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
@@ -1505,14 +1495,18 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
|
||||
);
|
||||
d.row_id = "";
|
||||
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
|
||||
} else if (
|
||||
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
|
||||
d.row_id
|
||||
) {
|
||||
if (d.idx == 1) {
|
||||
msg = __(
|
||||
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
|
||||
);
|
||||
d.charge_type = "";
|
||||
} else if (!d.row_id) {
|
||||
d.row_id = d.idx - 1;
|
||||
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
|
||||
d.row_id = "";
|
||||
} else if (d.row_id && d.row_id >= d.idx) {
|
||||
msg = __(
|
||||
"Cannot refer row number greater than or equal to current row number for this Charge type"
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
"party_name",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"column_break_11",
|
||||
"bank_account",
|
||||
"party_bank_account",
|
||||
@@ -62,6 +60,10 @@
|
||||
"taxes_and_charges_section",
|
||||
"purchase_taxes_and_charges_template",
|
||||
"sales_taxes_and_charges_template",
|
||||
"column_break_55",
|
||||
"apply_tax_withholding_amount",
|
||||
"tax_withholding_category",
|
||||
"section_break_56",
|
||||
"taxes",
|
||||
"section_break_60",
|
||||
"base_total_taxes_and_charges",
|
||||
@@ -69,11 +71,6 @@
|
||||
"total_taxes_and_charges",
|
||||
"deductions_or_loss_section",
|
||||
"deductions",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"transaction_references",
|
||||
"reference_no",
|
||||
"column_break_23",
|
||||
@@ -581,17 +578,24 @@
|
||||
"label": "Custom Remarks"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.apply_tds",
|
||||
"depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"mandatory_depends_on": "eval:doc.apply_tds",
|
||||
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
|
||||
"options": "Tax Withholding Category"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||
"fieldname": "apply_tax_withholding_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Tax Withholding Amount"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "taxes_and_charges_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Taxes and Charges"
|
||||
},
|
||||
{
|
||||
@@ -644,6 +648,15 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_55",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_56",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
|
||||
"fieldname": "received_amount_after_tax",
|
||||
@@ -682,7 +695,8 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
@@ -739,46 +753,6 @@
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.party_type == 'Supplier'",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -793,7 +767,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-18 13:56:40.206038",
|
||||
"modified": "2025-05-08 11:18:10.238085",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -30,7 +30,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PaymentTaxWithholding
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import (
|
||||
make_gl_entries,
|
||||
make_reverse_gl_entries,
|
||||
@@ -78,10 +80,9 @@ class PaymentEntry(AccountsController):
|
||||
from erpnext.accounts.doctype.payment_entry_reference.payment_entry_reference import (
|
||||
PaymentEntryReference,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
|
||||
amended_from: DF.Link | None
|
||||
apply_tds: DF.Check
|
||||
apply_tax_withholding_amount: DF.Check
|
||||
auto_repeat: DF.Link | None
|
||||
bank: DF.ReadOnly | None
|
||||
bank_account: DF.Link | None
|
||||
@@ -102,13 +103,11 @@ class PaymentEntry(AccountsController):
|
||||
custom_remarks: DF.Check
|
||||
deductions: DF.Table[PaymentEntryDeduction]
|
||||
difference_amount: DF.Currency
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
in_words: DF.SmallText | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
letter_head: DF.Link | None
|
||||
mode_of_payment: DF.Link | None
|
||||
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
|
||||
override_tax_withholding_entries: DF.Check
|
||||
paid_amount: DF.Currency
|
||||
paid_amount_after_tax: DF.Currency
|
||||
paid_from: DF.Link
|
||||
@@ -140,8 +139,6 @@ class PaymentEntry(AccountsController):
|
||||
status: DF.Literal["", "Draft", "Submitted", "Cancelled"]
|
||||
target_exchange_rate: DF.Float
|
||||
tax_withholding_category: DF.Link | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
taxes: DF.Table[AdvanceTaxesandCharges]
|
||||
title: DF.Data | None
|
||||
total_allocated_amount: DF.Currency
|
||||
@@ -192,7 +189,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
PaymentTaxWithholding(self).on_validate()
|
||||
self.set_tax_withholding()
|
||||
self.set_status()
|
||||
self.set_total_in_words()
|
||||
|
||||
@@ -202,7 +199,6 @@ class PaymentEntry(AccountsController):
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
PaymentTaxWithholding(self).on_submit()
|
||||
self.update_payment_requests()
|
||||
self.update_payment_schedule()
|
||||
self.make_gl_entries()
|
||||
@@ -304,10 +300,8 @@ class PaymentEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
PaymentTaxWithholding(self).on_cancel()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
@@ -943,6 +937,93 @@ class PaymentEntry(AccountsController):
|
||||
self.base_in_words = money_in_words(base_amount, self.company_currency)
|
||||
self.in_words = money_in_words(amount, currency)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if self.party_type != "Supplier":
|
||||
return
|
||||
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
net_total = self.calculate_tax_withholding_net_total()
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"doctype": "Payment Entry",
|
||||
"supplier": self.party,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
tax_withholding_details.update(
|
||||
{"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)}
|
||||
)
|
||||
|
||||
accounts = []
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
# Preserve user updated included in paid amount
|
||||
if d.included_in_paid_amount:
|
||||
tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount})
|
||||
|
||||
d.update(tax_withholding_details)
|
||||
accounts.append(d.account_head)
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append("taxes", tax_withholding_details)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.taxes
|
||||
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def calculate_tax_withholding_net_total(self):
|
||||
net_total = 0
|
||||
order_details = self.get_order_wise_tax_withholding_net_total()
|
||||
|
||||
for d in self.references:
|
||||
tax_withholding_net_total = order_details.get(d.reference_name)
|
||||
if not tax_withholding_net_total:
|
||||
continue
|
||||
|
||||
net_taxable_outstanding = max(
|
||||
0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total)
|
||||
)
|
||||
|
||||
net_total += min(net_taxable_outstanding, d.allocated_amount)
|
||||
|
||||
net_total += self.unallocated_amount
|
||||
|
||||
return net_total
|
||||
|
||||
def get_order_wise_tax_withholding_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
doctype = "Sales Order"
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
doctype,
|
||||
filters={"name": ["in", docnames]},
|
||||
fields=["name", "base_tax_withholding_net_total"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
@@ -1285,11 +1366,8 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gl_entries = self.build_gl_map()
|
||||
|
||||
merge_entries = frappe.get_single_value("Accounts Settings", "merge_similar_account_heads")
|
||||
|
||||
gl_entries = process_gl_map(gl_entries, merge_entries=merge_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj, merge_entries=merge_entries)
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
else:
|
||||
|
||||
@@ -1045,7 +1045,6 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
)
|
||||
|
||||
def test_gl_of_multi_currency_payment_with_taxes(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
||||
)
|
||||
@@ -1607,96 +1606,6 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
|
||||
def test_payment_entry_merges_gl_entries_with_same_account_head(self):
|
||||
"""
|
||||
Test that Payment Entry merges GL entries with same account head
|
||||
when 'Merge Similar Account Heads' setting is enabled.
|
||||
"""
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
|
||||
|
||||
pe = create_payment_entry(
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
paid_from="_Test Bank - _TC",
|
||||
paid_to="Creditors - _TC",
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 50,
|
||||
},
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 30,
|
||||
},
|
||||
)
|
||||
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "account": "Write Off - _TC", "is_cancelled": 0},
|
||||
fields=["debit", "credit"],
|
||||
)
|
||||
|
||||
self.assertEqual(len(gl_entries), 1)
|
||||
self.assertEqual(gl_entries[0].debit, 80)
|
||||
|
||||
def test_payment_entry_does_not_merge_gl_entries_when_setting_disabled(self):
|
||||
"""
|
||||
Test that Payment Entry does NOT merge GL entries
|
||||
when 'Merge Similar Account Heads' is disabled.
|
||||
"""
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
|
||||
pe = create_payment_entry(
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
paid_from="_Test Bank - _TC",
|
||||
paid_to="Creditors - _TC",
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 50,
|
||||
},
|
||||
)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "Write Off - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 30,
|
||||
},
|
||||
)
|
||||
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name, "account": "Write Off - _TC", "is_cancelled": 0},
|
||||
fields=["debit", "credit"],
|
||||
)
|
||||
|
||||
self.assertEqual(len(gl_entries), 2)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
|
||||
|
||||
def check_pl_entries(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
pl_entries = (
|
||||
|
||||
@@ -59,15 +59,14 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-13 06:52:46.130142",
|
||||
"modified": "2024-11-05 16:07:47.307971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"columns": 4,
|
||||
"columns": 2,
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_global_search": 1,
|
||||
@@ -49,10 +49,8 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Due Date",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -70,7 +68,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Grand Total",
|
||||
"print_hide": 1,
|
||||
@@ -79,7 +77,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Outstanding",
|
||||
"read_only": 1
|
||||
@@ -87,7 +85,7 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated"
|
||||
},
|
||||
@@ -176,7 +174,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-05 14:18:03.286224",
|
||||
"modified": "2025-07-25 04:32:11.040025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -18,12 +18,12 @@ class PaymentEntryReference(Document):
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
allocated_amount: DF.Currency
|
||||
allocated_amount: DF.Float
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
exchange_gain_loss: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
outstanding_amount: DF.Currency
|
||||
outstanding_amount: DF.Float
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
@@ -34,7 +34,7 @@ class PaymentEntryReference(Document):
|
||||
reconcile_effect_on: DF.Date | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Currency
|
||||
total_amount: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
@property
|
||||
|
||||
@@ -131,6 +131,7 @@ class PaymentLedgerEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
@@ -143,6 +144,7 @@ class PaymentLedgerEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
|
||||
@@ -50,10 +50,12 @@ class TestPaymentOrder(IntegrationTestCase):
|
||||
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
|
||||
payment_order = frappe.get_doc(
|
||||
doctype="Payment Order",
|
||||
company="_Test Company",
|
||||
payment_order_type=order_type,
|
||||
company_bank_account=bank_account,
|
||||
dict(
|
||||
doctype="Payment Order",
|
||||
company="_Test Company",
|
||||
payment_order_type=order_type,
|
||||
company_bank_account=bank_account,
|
||||
)
|
||||
)
|
||||
doc = make_payment_order(ref_doc.name, payment_order)
|
||||
doc.save()
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 Case, Criterion
|
||||
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,9 +393,6 @@ class PaymentReconciliation(Document):
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
party_account_defaults = frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, ["account_type", "account_currency"], as_dict=True
|
||||
)
|
||||
allocated_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
|
||||
)
|
||||
@@ -403,9 +400,9 @@ class PaymentReconciliation(Document):
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
|
||||
)
|
||||
difference_amount = 0
|
||||
if party_account_defaults.get("account_currency") != frappe.get_cached_value(
|
||||
"Company", self.company, "default_currency"
|
||||
):
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
) != frappe.get_cached_value("Company", self.company, "default_currency"):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
@@ -417,14 +414,7 @@ class PaymentReconciliation(Document):
|
||||
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
|
||||
# Added If clause to handle return Adhoc payments for account type holders ("Payable")
|
||||
if party_account_defaults.get("account_type") in ("Payable") and invoice.get(
|
||||
"invoice_type"
|
||||
) in ["Payment Entry", "Journal Entry"]:
|
||||
difference_amount = allocated_amount_in_inv_rate - allocated_amount_in_ref_rate
|
||||
else:
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
return difference_amount
|
||||
|
||||
@@ -687,28 +677,6 @@ class PaymentReconciliation(Document):
|
||||
)
|
||||
invoice_exchange_map.update(journals_map)
|
||||
|
||||
payment_entries = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Payment Entry"
|
||||
]
|
||||
payment_entries.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Payment Entry"]
|
||||
)
|
||||
if payment_entries:
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
query = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
pe.name,
|
||||
Case()
|
||||
.when(pe.payment_type == "Receive", pe.source_exchange_rate)
|
||||
.else_(pe.target_exchange_rate)
|
||||
.as_("exchange_rate"),
|
||||
)
|
||||
.where(pe.name.isin(payment_entries))
|
||||
)
|
||||
payment_entries = query.run(as_list=1)
|
||||
invoice_exchange_map.update(payment_entries)
|
||||
|
||||
return invoice_exchange_map
|
||||
|
||||
def validate_allocation(self):
|
||||
|
||||
@@ -2340,210 +2340,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
|
||||
frappe.db.set_value("Company", self.company, default_settings)
|
||||
|
||||
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_customer(self):
|
||||
transaction_date = nowdate()
|
||||
customer = self.customer3
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 100
|
||||
exchange_rate_at_reverse_payment = 95
|
||||
|
||||
# Receive amount from customer - 1,00,000
|
||||
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=customer)
|
||||
pe.payment_type = "Receive"
|
||||
pe.paid_from = self.debtors_eur
|
||||
pe.paid_from_account_currency = "EUR"
|
||||
pe.source_exchange_rate = exchange_rate_at_payment
|
||||
pe.paid_amount = amount
|
||||
pe.received_amount = exchange_rate_at_payment * amount
|
||||
pe.paid_to = self.cash
|
||||
pe.paid_to_account_currency = "INR"
|
||||
pe = pe.save().submit()
|
||||
|
||||
# Pay amount to customer - 95,000
|
||||
reverse_pe = self.create_payment_entry(
|
||||
amount=amount, posting_date=transaction_date, customer=customer
|
||||
)
|
||||
reverse_pe.payment_type = "Pay"
|
||||
reverse_pe.paid_from = self.cash
|
||||
reverse_pe.paid_from_account_currency = "INR"
|
||||
reverse_pe.target_exchange_rate = exchange_rate_at_reverse_payment
|
||||
reverse_pe.paid_amount = exchange_rate_at_reverse_payment * amount
|
||||
reverse_pe.received_amount = amount
|
||||
reverse_pe.paid_to = self.debtors_eur
|
||||
reverse_pe.paid_to_account_currency = "EUR"
|
||||
reverse_pe.save().submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a gain of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].get("difference_amount")), 5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_supplier(self):
|
||||
transaction_date = nowdate()
|
||||
self.supplier = "_Test Supplier USD"
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 100
|
||||
exchange_rate_at_reverse_payment = 95
|
||||
|
||||
# Pay amount to supplier - 1,00,000
|
||||
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
|
||||
pe.payment_type = "Pay"
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_from_account_currency = "INR"
|
||||
pe.target_exchange_rate = exchange_rate_at_payment
|
||||
pe.paid_amount = exchange_rate_at_payment * amount
|
||||
pe.received_amount = amount
|
||||
pe.paid_to = self.creditors_usd
|
||||
pe.paid_to_account_currency = "USD"
|
||||
pe.save().submit()
|
||||
|
||||
# Receive amount from supplier - 95,000
|
||||
reverse_pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
|
||||
reverse_pe.payment_type = "Receive"
|
||||
reverse_pe.party_type = "Supplier"
|
||||
reverse_pe.party = self.supplier
|
||||
reverse_pe.paid_from = self.creditors_usd
|
||||
reverse_pe.paid_from_account_currency = "USD"
|
||||
reverse_pe.source_exchange_rate = exchange_rate_at_reverse_payment
|
||||
reverse_pe.paid_amount = amount
|
||||
reverse_pe.received_amount = exchange_rate_at_reverse_payment * amount
|
||||
reverse_pe.paid_to = self.cash
|
||||
reverse_pe.paid_to_account_currency = "INR"
|
||||
reverse_pe = reverse_pe.save().submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a loss of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].get("difference_amount")), -5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self):
|
||||
transaction_date = nowdate()
|
||||
customer = self.customer3
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 95
|
||||
exchange_rate_at_reverse_payment = 100
|
||||
|
||||
# Receive amount from customer - 95,000
|
||||
je1 = self.create_journal_entry(self.cash, self.debtors_eur, amount, transaction_date)
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].debit_in_account_currency = exchange_rate_at_payment * amount
|
||||
je1.accounts[0].debit = exchange_rate_at_payment * amount
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = customer
|
||||
je1.accounts[1].exchange_rate = exchange_rate_at_payment
|
||||
je1.accounts[1].credit_in_account_currency = amount
|
||||
je1.accounts[1].credit = exchange_rate_at_payment * amount
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
# Pay amount to customer - 1,00,000
|
||||
je2 = self.create_journal_entry(self.debtors_eur, self.cash, amount, transaction_date)
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].party_type = "Customer"
|
||||
je2.accounts[0].party = customer
|
||||
je2.accounts[0].exchange_rate = exchange_rate_at_reverse_payment
|
||||
je2.accounts[0].debit_in_account_currency = amount
|
||||
je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[1].exchange_rate = 1
|
||||
je2.accounts[1].credit_in_account_currency = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a loss of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].difference_amount), -5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_supplier(self):
|
||||
transaction_date = nowdate()
|
||||
self.supplier = "_Test Supplier USD"
|
||||
amount = 1000
|
||||
exchange_rate_at_payment = 95
|
||||
exchange_rate_at_reverse_payment = 100
|
||||
|
||||
# Pay amount to supplier - 95,000
|
||||
je1 = self.create_journal_entry(self.creditors_usd, self.cash, amount, transaction_date)
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].party_type = "Supplier"
|
||||
je1.accounts[0].party = self.supplier
|
||||
je1.accounts[0].exchange_rate = exchange_rate_at_payment
|
||||
je1.accounts[0].debit_in_account_currency = amount
|
||||
je1.accounts[0].debit = exchange_rate_at_payment * amount
|
||||
je1.accounts[1].exchange_rate = 1
|
||||
je1.accounts[1].credit = exchange_rate_at_payment * amount
|
||||
je1.accounts[1].credit_in_account_currency = exchange_rate_at_payment * amount
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
# Receive amount from supplier - 1,00,000
|
||||
je2 = self.create_journal_entry(self.cash, self.creditors_usd, amount, transaction_date)
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].debit = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[0].debit_in_account_currency = exchange_rate_at_reverse_payment * amount
|
||||
je2.accounts[1].party_type = "Supplier"
|
||||
je2.accounts[1].party = self.supplier
|
||||
je2.accounts[1].exchange_rate = exchange_rate_at_reverse_payment
|
||||
je2.accounts[1].credit_in_account_currency = amount
|
||||
je2.accounts[1].credit = exchange_rate_at_reverse_payment * amount
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
# Reconcile payments
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Check the difference_amount is a gain of 5000
|
||||
self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -100,10 +100,7 @@ class PaymentRequest(Document):
|
||||
subscription_plans: DF.Table[SubscriptionPlanDetail]
|
||||
swift_number: DF.ReadOnly | None
|
||||
transaction_date: DF.Date | None
|
||||
|
||||
# end: auto-generated types
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def validate(self):
|
||||
if self.get("__islocal"):
|
||||
@@ -430,7 +427,6 @@ class PaymentRequest(Document):
|
||||
context = {
|
||||
"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
|
||||
"payment_url": self.payment_url,
|
||||
"payment_request": self,
|
||||
}
|
||||
|
||||
if self.message:
|
||||
@@ -543,9 +539,6 @@ def make_payment_request(**args):
|
||||
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
|
||||
if args.dn and not isinstance(args.dn, str):
|
||||
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
||||
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
@@ -850,7 +843,6 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
)
|
||||
|
||||
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||
doc_updates = {}
|
||||
|
||||
for ref in references:
|
||||
if not ref.payment_request:
|
||||
@@ -876,7 +868,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
|
||||
# determine status
|
||||
# update status
|
||||
if new_outstanding_amount == payment_request["grand_total"]:
|
||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||
elif new_outstanding_amount == 0:
|
||||
@@ -884,37 +876,31 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
elif new_outstanding_amount > 0:
|
||||
status = "Partially Paid"
|
||||
|
||||
# prepare bulk update data
|
||||
doc_updates[ref.payment_request] = {
|
||||
"outstanding_amount": new_outstanding_amount,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
# bulk update all payment requests
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update("Payment Request", doc_updates)
|
||||
# update database
|
||||
frappe.db.set_value(
|
||||
"Payment Request",
|
||||
ref.payment_request,
|
||||
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||
)
|
||||
|
||||
|
||||
def get_dummy_message(doc):
|
||||
return """
|
||||
{% if doc.contact_person -%}
|
||||
<p>Dear {{ doc.contact_person }},</p>
|
||||
{%- else %}<p>Hello,</p>{% endif %}
|
||||
return frappe.render_template(
|
||||
"""{% if doc.contact_person -%}
|
||||
<p>Dear {{ doc.contact_person }},</p>
|
||||
{%- else %}<p>Hello,</p>{% endif %}
|
||||
|
||||
<p>
|
||||
{{ _("Requesting payment against {0} {1} for amount {2}").format(
|
||||
doc.doctype,
|
||||
doc.name,
|
||||
payment_request.get_formatted("grand_total")
|
||||
) }}
|
||||
</p>
|
||||
<p>{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype,
|
||||
doc.name, doc.get_formatted("grand_total")) }}</p>
|
||||
|
||||
<a href="{{ payment_url }}">{{ _("Make Payment") }}</a>
|
||||
<a href="{{ payment_url }}">{{ _("Make Payment") }}</a>
|
||||
|
||||
<p>{{ _("If you have any questions, please get back to us.") }}</p>
|
||||
<p>{{ _("If you have any questions, please get back to us.") }}</p>
|
||||
|
||||
<p>{{ _("Thank you for your business!") }}</p>
|
||||
"""
|
||||
<p>{{ _("Thank you for your business!") }}</p>
|
||||
""",
|
||||
dict(doc=doc, payment_url="{{ payment_url }}"),
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -13,9 +13,9 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
return {
|
||||
filters: [
|
||||
["Account", "company", "=", frm.doc.company],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "is_group", "=", "0"],
|
||||
["Account", "freeze_account", "=", "No"],
|
||||
["Account", "root_type", "in", ["Liability", "Equity"]],
|
||||
["Account", "root_type", "in", "Liability, Equity"],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -539,7 +539,6 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
rate=1000,
|
||||
serial_no=[serial_nos[0]],
|
||||
do_not_save=1,
|
||||
ignore_sabb_validation=True,
|
||||
)
|
||||
|
||||
pos2.append("payments", {"mode_of_payment": "Bank Draft", "amount": 1000})
|
||||
@@ -1017,7 +1016,6 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
ignore_sabb_validation=True,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
||||
@@ -1159,7 +1157,6 @@ def create_pos_invoice(**args):
|
||||
"posting_time": pos_inv.posting_time,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"do_not_submit": True,
|
||||
"ignore_sabb_validation": args.ignore_sabb_validation,
|
||||
}
|
||||
)
|
||||
).name
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"invoice_type",
|
||||
"column_break_vwwt",
|
||||
"post_change_gl_entries",
|
||||
"section_break_gyos",
|
||||
"invoice_fields",
|
||||
"pos_search_fields"
|
||||
@@ -36,24 +34,11 @@
|
||||
{
|
||||
"fieldname": "section_break_gyos",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vwwt",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Ledger Entries for Change Amount",
|
||||
"options": "1"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-09 17:30:41.476806",
|
||||
"modified": "2025-06-06 11:36:44.885353",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Settings",
|
||||
|
||||
@@ -23,7 +23,6 @@ class POSSettings(Document):
|
||||
invoice_fields: DF.Table[POSField]
|
||||
invoice_type: DF.Literal["Sales Invoice", "POS Invoice"]
|
||||
pos_search_fields: DF.Table[POSSearchFields]
|
||||
post_change_gl_entries: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -171,7 +171,7 @@ frappe.ui.form.on("Pricing Rule", {
|
||||
|
||||
set_field_options("applicable_for", options.join("\n"));
|
||||
|
||||
if (!options.includes(applicable_for)) applicable_for = null;
|
||||
if (!in_list(options, applicable_for)) applicable_for = null;
|
||||
frm.set_value("applicable_for", applicable_for);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,11 +48,13 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
|
||||
process_deferred_accounting = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2023-07-01",
|
||||
start_date="2023-05-01",
|
||||
end_date="2023-06-30",
|
||||
type="Income",
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2023-07-01",
|
||||
start_date="2023-05-01",
|
||||
end_date="2023-06-30",
|
||||
type="Income",
|
||||
)
|
||||
)
|
||||
|
||||
process_deferred_accounting.insert()
|
||||
@@ -78,11 +80,13 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
pda = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2019-01-01",
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-01-31",
|
||||
type="Income",
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2019-01-01",
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-01-31",
|
||||
type="Income",
|
||||
)
|
||||
)
|
||||
pda.submit()
|
||||
pda.cancel()
|
||||
|
||||
@@ -35,10 +35,7 @@ class ProcessPaymentReconciliation(Document):
|
||||
]
|
||||
to_invoice_date: DF.Date | None
|
||||
to_payment_date: DF.Date | None
|
||||
|
||||
# end: auto-generated types
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def validate(self):
|
||||
self.validate_receivable_payable_account()
|
||||
|
||||
@@ -36,10 +36,7 @@ class ProcessPeriodClosingVoucher(Document):
|
||||
parent_pcv: DF.Link
|
||||
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
|
||||
z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
|
||||
|
||||
# end: auto-generated types
|
||||
def on_discard(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def validate(self):
|
||||
self.status = "Queued"
|
||||
|
||||
@@ -46,7 +46,7 @@ frappe.ui.form.on("Promotional Scheme", {
|
||||
|
||||
set_field_options("applicable_for", options.join("\n"));
|
||||
|
||||
if (!options.includes(applicable_for)) applicable_for = null;
|
||||
if (!in_list(options, applicable_for)) applicable_for = null;
|
||||
frm.set_value("applicable_for", applicable_for);
|
||||
},
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
@@ -224,6 +224,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
});
|
||||
}
|
||||
|
||||
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
@@ -363,9 +364,10 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
},
|
||||
function () {
|
||||
me.apply_pricing_rule();
|
||||
me.frm.doc.apply_tds =
|
||||
me.frm.tax_withholding_category || me.frm.tax_withholding_group ? 1 : 0;
|
||||
me.frm.clear_table("tax_withholding_entries");
|
||||
me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0;
|
||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
||||
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
|
||||
|
||||
// while duplicating, don't change payment terms
|
||||
if (me.frm.doc.__run_link_triggers === false) {
|
||||
@@ -378,7 +380,26 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
apply_tds(frm) {
|
||||
var me = this;
|
||||
me.frm.clear_table("tax_withholding_entries");
|
||||
me.frm.set_value("tax_withheld_vouchers", []);
|
||||
if (!me.frm.doc.apply_tds) {
|
||||
me.frm.set_value("tax_withholding_category", "");
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", 1);
|
||||
} else {
|
||||
me.frm.set_value("tax_withholding_category", me.frm.supplier_tds);
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", 0);
|
||||
}
|
||||
}
|
||||
|
||||
tax_withholding_category(frm) {
|
||||
var me = this;
|
||||
let filtered_taxes = (me.frm.doc.taxes || []).filter((row) => !row.is_tax_withholding_account);
|
||||
me.frm.clear_table("taxes");
|
||||
|
||||
filtered_taxes.forEach((row) => {
|
||||
me.frm.add_child("taxes", row);
|
||||
});
|
||||
|
||||
me.frm.refresh_field("taxes");
|
||||
}
|
||||
|
||||
credit_to() {
|
||||
@@ -558,6 +579,17 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
if (d.cost_center) {
|
||||
var cl = doc.items || [];
|
||||
for (var i = 0; i < cl.length; i++) {
|
||||
if (!cl[i].cost_center) cl[i].cost_center = d.cost_center;
|
||||
}
|
||||
}
|
||||
refresh_field("items");
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
@@ -671,7 +703,10 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__onload && frm.doc.supplier) {
|
||||
if (frm.is_new()) {
|
||||
frm.doc.apply_tds = frm.doc.__onload.apply_tds ? 1 : 0;
|
||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||
}
|
||||
if (!frm.doc.__onload.supplier_tds) {
|
||||
frm.set_df_property("apply_tds", "read_only", 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,7 +715,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
});
|
||||
|
||||
if (frm.is_new()) {
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
frm.clear_table("tax_withheld_vouchers");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -707,7 +742,6 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
|
||||
company: function (frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
|
||||
if (frm.doc.company) {
|
||||
frappe.call({
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"update_billed_amount_in_purchase_order",
|
||||
"update_billed_amount_in_purchase_receipt",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"amended_from",
|
||||
"supplier_invoice_details",
|
||||
"bill_no",
|
||||
@@ -67,6 +68,8 @@
|
||||
"column_break_28",
|
||||
"total",
|
||||
"net_total",
|
||||
"tax_withholding_net_total",
|
||||
"base_tax_withholding_net_total",
|
||||
"taxes_section",
|
||||
"tax_category",
|
||||
"taxes_and_charges",
|
||||
@@ -99,17 +102,14 @@
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"section_break_44",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"column_break_46",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
"tax_withheld_vouchers_section",
|
||||
"tax_withheld_vouchers",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"item_wise_tax_details",
|
||||
@@ -130,6 +130,7 @@
|
||||
"only_include_allocated_payments",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"advance_tax",
|
||||
"write_off",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
@@ -285,7 +286,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding",
|
||||
"label": "Apply Tax Withholding Amount",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -1357,6 +1358,14 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Tax Withholding Category",
|
||||
"options": "Tax Withholding Category",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_address",
|
||||
"fieldtype": "Link",
|
||||
@@ -1446,6 +1455,14 @@
|
||||
"fieldname": "column_break_147",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_tax",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Advance Tax",
|
||||
"options": "Advance Tax",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription",
|
||||
"fieldtype": "Link",
|
||||
@@ -1460,6 +1477,42 @@
|
||||
"label": "Is Old Subcontracting Flow",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "apply_tds",
|
||||
"fieldname": "tax_withholding_net_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Tax Withholding Net Total",
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "apply_tds",
|
||||
"fieldname": "base_tax_withholding_net_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Base Tax Withholding Net Total",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible_depends_on": "tax_withheld_vouchers",
|
||||
"fieldname": "tax_withheld_vouchers_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withheld Vouchers"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withheld_vouchers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withheld Vouchers",
|
||||
"no_copy": 1,
|
||||
"options": "Tax Withheld Vouchers",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
@@ -1609,7 +1662,7 @@
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname": "claimed_landed_cost_amount",
|
||||
"fieldtype": "Currency",
|
||||
@@ -1626,40 +1679,6 @@
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1667,7 +1686,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-15 06:41:38.237728",
|
||||
"modified": "2025-08-04 19:19:11.380664",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -24,7 +24,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_linked_doc,
|
||||
validate_inter_company_party,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import PurchaseTaxWithholding
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import (
|
||||
get_round_off_account_and_cost_center,
|
||||
make_gl_entries,
|
||||
@@ -59,6 +61,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.advance_tax.advance_tax import AdvanceTax
|
||||
from erpnext.accounts.doctype.item_wise_tax_detail.item_wise_tax_detail import ItemWiseTaxDetail
|
||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||
@@ -69,13 +72,14 @@ class PurchaseInvoice(BuyingController):
|
||||
from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import (
|
||||
PurchaseTaxesandCharges,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import TaxWithheldVouchers
|
||||
from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import (
|
||||
PurchaseReceiptItemSupplied,
|
||||
)
|
||||
|
||||
additional_discount_percentage: DF.Float
|
||||
address_display: DF.TextEditor | None
|
||||
advance_tax: DF.Table[AdvanceTax]
|
||||
advances: DF.Table[PurchaseInvoiceAdvance]
|
||||
against_expense_account: DF.SmallText | None
|
||||
allocate_advances_automatically: DF.Check
|
||||
@@ -90,6 +94,7 @@ class PurchaseInvoice(BuyingController):
|
||||
base_paid_amount: DF.Currency
|
||||
base_rounded_total: DF.Currency
|
||||
base_rounding_adjustment: DF.Currency
|
||||
base_tax_withholding_net_total: DF.Currency
|
||||
base_taxes_and_charges_added: DF.Currency
|
||||
base_taxes_and_charges_deducted: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -123,7 +128,6 @@ class PurchaseInvoice(BuyingController):
|
||||
hold_comment: DF.SmallText | None
|
||||
ignore_default_payment_terms_template: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
in_words: DF.Data | None
|
||||
incoterm: DF.Link | None
|
||||
inter_company_invoice_reference: DF.Link | None
|
||||
@@ -145,7 +149,6 @@ class PurchaseInvoice(BuyingController):
|
||||
only_include_allocated_payments: DF.Check
|
||||
other_charges_calculation: DF.TextEditor | None
|
||||
outstanding_amount: DF.Currency
|
||||
override_tax_withholding_entries: DF.Check
|
||||
paid_amount: DF.Currency
|
||||
party_account_currency: DF.Link | None
|
||||
payment_schedule: DF.Table[PaymentSchedule]
|
||||
@@ -195,8 +198,9 @@ class PurchaseInvoice(BuyingController):
|
||||
supplier_warehouse: DF.Link | None
|
||||
tax_category: DF.Link | None
|
||||
tax_id: DF.ReadOnly | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
tax_withheld_vouchers: DF.Table[TaxWithheldVouchers]
|
||||
tax_withholding_category: DF.Link | None
|
||||
tax_withholding_net_total: DF.Currency
|
||||
taxes: DF.Table[PurchaseTaxesandCharges]
|
||||
taxes_and_charges: DF.Link | None
|
||||
taxes_and_charges_added: DF.Currency
|
||||
@@ -241,14 +245,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def onload(self):
|
||||
super().onload()
|
||||
if self.supplier:
|
||||
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||
"Supplier", self.supplier, ["tax_withholding_category", "tax_withholding_group"]
|
||||
)
|
||||
self.set_onload("apply_tds", tax_withholding_category or tax_withholding_group)
|
||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
|
||||
if self.is_new():
|
||||
self.set("tax_withholding_entries", [])
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
def before_save(self):
|
||||
if not self.on_hold:
|
||||
@@ -299,7 +300,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
PurchaseTaxWithholding(self).on_validate()
|
||||
self.set_percentage_received()
|
||||
|
||||
def set_percentage_received(self):
|
||||
@@ -352,13 +352,11 @@ class PurchaseInvoice(BuyingController):
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
|
||||
if self.supplier:
|
||||
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||
"Supplier", self.supplier, ["tax_withholding_category", "tax_withholding_group"]
|
||||
)
|
||||
if not for_validate:
|
||||
if tax_withholding_category or tax_withholding_group:
|
||||
self.apply_tds = 1
|
||||
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
if tds_category and not for_validate:
|
||||
self.apply_tds = 1
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
@@ -749,7 +747,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def on_submit(self):
|
||||
super().on_submit()
|
||||
PurchaseTaxWithholding(self).on_submit()
|
||||
|
||||
self.check_prev_docstatus()
|
||||
|
||||
@@ -791,6 +788,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.update_project()
|
||||
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
self.update_advance_tax_references()
|
||||
|
||||
self.process_common_party_accounting()
|
||||
|
||||
@@ -1674,7 +1672,6 @@ class PurchaseInvoice(BuyingController):
|
||||
check_if_return_invoice_linked_with_payment_entry(self)
|
||||
|
||||
super().on_cancel()
|
||||
PurchaseTaxWithholding(self).on_cancel()
|
||||
|
||||
self.check_on_hold_or_closed_status()
|
||||
|
||||
@@ -1721,9 +1718,10 @@ class PurchaseInvoice(BuyingController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Tax Withheld Vouchers",
|
||||
"Serial and Batch Bundle",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
self.update_advance_tax_references(cancel=1)
|
||||
|
||||
def update_project(self):
|
||||
projects = frappe._dict()
|
||||
@@ -1846,6 +1844,102 @@ class PurchaseInvoice(BuyingController):
|
||||
self.db_set("on_hold", 0)
|
||||
self.db_set("release_date", None)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
self.set("advance_tax", [])
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
if self.apply_tds and not self.get("tax_withholding_category"):
|
||||
self.tax_withholding_category = frappe.db.get_value(
|
||||
"Supplier", self.supplier, "tax_withholding_category"
|
||||
)
|
||||
|
||||
if not self.tax_withholding_category:
|
||||
return
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
self, self.tax_withholding_category
|
||||
)
|
||||
|
||||
# Adjust TDS paid on advances
|
||||
self.allocate_advance_tds(tax_withholding_details, advance_taxes)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
d.update(tax_withholding_details)
|
||||
|
||||
accounts.append(d.account_head)
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append("taxes", tax_withholding_details)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.taxes
|
||||
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
for row in voucher_wise_amount:
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
{
|
||||
"voucher_name": row.voucher_name,
|
||||
"voucher_type": row.voucher_type,
|
||||
"taxable_amount": row.taxable_amount,
|
||||
},
|
||||
)
|
||||
|
||||
# calculate totals again after applying TDS
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
|
||||
for tax in advance_taxes:
|
||||
allocated_amount = 0
|
||||
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
|
||||
if flt(tax_withholding_details.get("tax_amount")) >= pending_amount:
|
||||
tax_withholding_details["tax_amount"] -= pending_amount
|
||||
allocated_amount = pending_amount
|
||||
elif (
|
||||
flt(tax_withholding_details.get("tax_amount"))
|
||||
and flt(tax_withholding_details.get("tax_amount")) < pending_amount
|
||||
):
|
||||
allocated_amount = tax_withholding_details["tax_amount"]
|
||||
tax_withholding_details["tax_amount"] = 0
|
||||
|
||||
self.append(
|
||||
"advance_tax",
|
||||
{
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": tax.parent,
|
||||
"reference_detail": tax.name,
|
||||
"account_head": tax.account_head,
|
||||
"allocated_amount": allocated_amount,
|
||||
},
|
||||
)
|
||||
|
||||
def update_advance_tax_references(self, cancel=0):
|
||||
for tax in self.get("advance_tax"):
|
||||
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
|
||||
|
||||
if cancel:
|
||||
frappe.qb.update(at).set(
|
||||
at.allocated_amount, at.allocated_amount - tax.allocated_amount
|
||||
).where(at.name == tax.reference_detail).run()
|
||||
else:
|
||||
frappe.qb.update(at).set(
|
||||
at.allocated_amount, at.allocated_amount + tax.allocated_amount
|
||||
).where(at.name == tax.reference_detail).run()
|
||||
|
||||
def set_status(self, update=False, status=None, update_modified=True):
|
||||
if self.is_new():
|
||||
if self.get("amended_from"):
|
||||
@@ -1933,7 +2027,6 @@ def get_list_context(context=None):
|
||||
"show_search": True,
|
||||
"no_breadcrumbs": True,
|
||||
"title": _("Purchase Invoices"),
|
||||
"list_template": "templates/includes/list/list.html",
|
||||
}
|
||||
)
|
||||
return list_context
|
||||
@@ -2004,26 +2097,10 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
for row in target_parent.get("items"):
|
||||
if row.get("qty") == 0:
|
||||
target_parent.remove(row)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
||||
|
||||
returned_qty_map = (
|
||||
get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, obj.name, "Purchase Invoice"
|
||||
)
|
||||
or {}
|
||||
)
|
||||
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
|
||||
obj.conversion_factor
|
||||
)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
@@ -2062,7 +2139,6 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
},
|
||||
target_doc,
|
||||
post_parent_process,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -1249,12 +1249,14 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
pi.submit()
|
||||
|
||||
pda1 = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Expense",
|
||||
company="_Test Company",
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Expense",
|
||||
company="_Test Company",
|
||||
)
|
||||
)
|
||||
|
||||
pda1.insert()
|
||||
@@ -1498,8 +1500,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 1)
|
||||
|
||||
company = "_Test Company"
|
||||
|
||||
tds_account_args = {
|
||||
@@ -1538,7 +1538,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
# Create Payment Entry Against the order
|
||||
payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name)
|
||||
payment_entry.paid_from = "Cash - _TC"
|
||||
payment_entry.apply_tds = 1
|
||||
payment_entry.apply_tax_withholding_amount = 1
|
||||
payment_entry.tax_withholding_category = tax_withholding_category
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
@@ -1591,26 +1591,12 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
self.assertEqual(expected_gle[i][1], gle.amount)
|
||||
|
||||
payment_entry.load_from_db()
|
||||
tax_allocated = sum(
|
||||
[
|
||||
entry.withholding_amount
|
||||
for entry in payment_entry.get("tax_withholding_entries", [])
|
||||
if entry.taxable_name
|
||||
]
|
||||
)
|
||||
self.assertEqual(tax_allocated, 3000)
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
|
||||
|
||||
purchase_invoice.cancel()
|
||||
|
||||
payment_entry.load_from_db()
|
||||
tax_allocated = sum(
|
||||
[
|
||||
entry.withholding_amount
|
||||
for entry in payment_entry.get("tax_withholding_entries", [])
|
||||
if entry.taxable_name
|
||||
]
|
||||
)
|
||||
self.assertEqual(tax_allocated, 0)
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||
|
||||
def test_purchase_gl_with_tax_withholding_tax(self):
|
||||
company = "_Test Company"
|
||||
@@ -1645,6 +1631,7 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
do_not_submit=1,
|
||||
)
|
||||
pi.apply_tds = 1
|
||||
pi.tax_withholding_category = tax_withholding_category
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
@@ -2944,29 +2931,6 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
pi.save()
|
||||
self.assertEqual(pi.discount_amount, discount_amount)
|
||||
|
||||
def test_returned_item_purchase_receipt(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||
make_purchase_receipt as make_purchase_receipt_from_pi,
|
||||
)
|
||||
|
||||
item = create_item("_Test Returned Item Purchase Receipt", is_stock_item=1)
|
||||
|
||||
pi = make_purchase_invoice(item_code=item.name, qty=5, rate=100)
|
||||
|
||||
return_pi = make_purchase_invoice(
|
||||
item_code=item.name,
|
||||
is_return=1,
|
||||
return_against=pi.name,
|
||||
qty=-5,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
return_pi.items[0].purchase_invoice_item = pi.items[0].name
|
||||
return_pi.submit()
|
||||
|
||||
pr = make_purchase_receipt_from_pi(pi.name)
|
||||
self.assertFalse(pr.items)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
"rate",
|
||||
"amount",
|
||||
"item_tax_template",
|
||||
"tax_withholding_category",
|
||||
"col_break4",
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
@@ -229,6 +228,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
@@ -893,7 +893,7 @@
|
||||
"default": "1",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding"
|
||||
"label": "Apply TDS"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
||||
@@ -979,20 +979,13 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"options": "Tax Withholding Category",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-13 14:10:02.379392",
|
||||
"modified": "2025-10-14 13:00:54.441511",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -90,7 +90,6 @@ class PurchaseInvoiceItem(Document):
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link | None
|
||||
stock_uom_rate: DF.Currency
|
||||
tax_withholding_category: DF.Link | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
"base_net_amount",
|
||||
"base_tax_amount",
|
||||
"base_total",
|
||||
"base_tax_amount_after_discount_amount",
|
||||
"dont_recompute_tax"
|
||||
"base_tax_amount_after_discount_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -206,11 +205,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -263,22 +262,13 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "dont_recompute_tax",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Don't Recompute Tax",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-24 18:22:56.886010",
|
||||
"modified": "2025-07-24 15:08:44.433022",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
|
||||
@@ -32,7 +32,6 @@ class PurchaseTaxesandCharges(Document):
|
||||
]
|
||||
cost_center: DF.Link | None
|
||||
description: DF.SmallText
|
||||
dont_recompute_tax: DF.Check
|
||||
included_in_paid_amount: DF.Check
|
||||
included_in_print_rate: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
@@ -40,7 +39,6 @@ class PurchaseTaxesandCharges(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
set_by_item_tax_template: DF.Check
|
||||
|
||||
@@ -115,10 +115,6 @@ class RepostAccountingLedger(Document):
|
||||
def generate_preview(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns
|
||||
|
||||
if not self.vouchers:
|
||||
frappe.msgprint(_("Add vouchers to generate preview."))
|
||||
return
|
||||
|
||||
gl_columns = []
|
||||
gl_data = []
|
||||
|
||||
@@ -146,7 +142,6 @@ class RepostAccountingLedger(Document):
|
||||
account_repost_doc=self.name,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(_("Repost has started in the background"))
|
||||
else:
|
||||
|
||||
@@ -14,12 +14,10 @@
|
||||
"options": "Repost Allowed Types"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:19:08.888368",
|
||||
"modified": "2024-06-06 13:56:37.908879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
@@ -45,9 +43,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ class RepostAccountingLedgerSettings(Document):
|
||||
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
|
||||
|
||||
allowed_types: DF.Table[RepostAllowedTypes]
|
||||
# end: auto-generated types
|
||||
|
||||
# end: auto-generated types
|
||||
def validate(self):
|
||||
self.update_property_for_accounting_dimension()
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
company() {
|
||||
super.company();
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
this.frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
onload() {
|
||||
var me = this;
|
||||
@@ -383,9 +382,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
),
|
||||
},
|
||||
function () {
|
||||
me.frm.doc.apply_tds =
|
||||
me.frm.tax_withholding_category || me.frm.tax_withholding_group ? 1 : 0;
|
||||
me.frm.clear_table("tax_withholding_entries");
|
||||
me.apply_pricing_rule();
|
||||
}
|
||||
);
|
||||
@@ -602,10 +598,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
this.calculate_taxes_and_totals();
|
||||
}
|
||||
|
||||
apply_tds(frm) {
|
||||
this.frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
@@ -626,6 +618,10 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
|
||||
};
|
||||
|
||||
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("customer", "tax_id", "tax_id");
|
||||
@@ -822,16 +818,6 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
},
|
||||
onload: function (frm) {
|
||||
frm.redemption_conversion_factor = null;
|
||||
|
||||
if (frm.doc.__onload && frm.doc.customer) {
|
||||
if (frm.is_new()) {
|
||||
frm.doc.apply_tds = frm.doc.__onload.apply_tds ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.is_new()) {
|
||||
frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
},
|
||||
|
||||
update_stock: function (frm, dt, dn) {
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"update_billed_amount_in_sales_order",
|
||||
"update_billed_amount_in_delivery_note",
|
||||
"is_debit_note",
|
||||
"apply_tds",
|
||||
"amended_from",
|
||||
"is_created_using_pos",
|
||||
"pos_closing_entry",
|
||||
@@ -91,11 +90,6 @@
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"section_break_49",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
@@ -2253,46 +2247,6 @@
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Group",
|
||||
"options": "Tax Withholding Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_entries",
|
||||
"fieldtype": "Table",
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_tax_withholding_threshold",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Tax Withholding Threshold"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -26,7 +26,9 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import SalesTaxWithholding
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import (
|
||||
@@ -75,7 +77,6 @@ class SalesInvoice(SellingController):
|
||||
from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import (
|
||||
SalesTaxesandCharges,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import TaxWithholdingEntry
|
||||
from erpnext.selling.doctype.sales_team.sales_team import SalesTeam
|
||||
from erpnext.stock.doctype.packed_item.packed_item import PackedItem
|
||||
|
||||
@@ -89,7 +90,6 @@ class SalesInvoice(SellingController):
|
||||
amended_from: DF.Link | None
|
||||
amount_eligible_for_commission: DF.Currency
|
||||
apply_discount_on: DF.Literal["", "Grand Total", "Net Total"]
|
||||
apply_tds: DF.Check
|
||||
auto_repeat: DF.Link | None
|
||||
base_change_amount: DF.Currency
|
||||
base_discount_amount: DF.Currency
|
||||
@@ -135,7 +135,6 @@ class SalesInvoice(SellingController):
|
||||
has_subcontracted: DF.Check
|
||||
ignore_default_payment_terms_template: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
ignore_tax_withholding_threshold: DF.Check
|
||||
in_words: DF.SmallText | None
|
||||
incoterm: DF.Link | None
|
||||
inter_company_invoice_reference: DF.Link | None
|
||||
@@ -163,7 +162,6 @@ class SalesInvoice(SellingController):
|
||||
only_include_allocated_payments: DF.Check
|
||||
other_charges_calculation: DF.TextEditor | None
|
||||
outstanding_amount: DF.Currency
|
||||
override_tax_withholding_entries: DF.Check
|
||||
packed_items: DF.Table[PackedItem]
|
||||
paid_amount: DF.Currency
|
||||
party_account_currency: DF.Link | None
|
||||
@@ -216,8 +214,6 @@ class SalesInvoice(SellingController):
|
||||
subscription: DF.Link | None
|
||||
tax_category: DF.Link | None
|
||||
tax_id: DF.Data | None
|
||||
tax_withholding_entries: DF.Table[TaxWithholdingEntry]
|
||||
tax_withholding_group: DF.Link | None
|
||||
taxes: DF.Table[SalesTaxesandCharges]
|
||||
taxes_and_charges: DF.Link | None
|
||||
tc_name: DF.Link | None
|
||||
@@ -286,14 +282,6 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_color = "green"
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def onload(self):
|
||||
super().onload()
|
||||
if self.customer:
|
||||
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||
"Customer", self.customer, ["tax_withholding_category", "tax_withholding_group"]
|
||||
)
|
||||
self.set_onload("apply_tds", tax_withholding_category or tax_withholding_group)
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
@@ -303,7 +291,7 @@ class SalesInvoice(SellingController):
|
||||
if not (self.is_pos or self.is_debit_note):
|
||||
self.so_dn_required()
|
||||
|
||||
SalesTaxWithholding(self).on_validate()
|
||||
self.set_tax_withholding()
|
||||
|
||||
self.validate_proj_cust()
|
||||
self.validate_pos_return()
|
||||
@@ -425,6 +413,38 @@ class SalesInvoice(SellingController):
|
||||
for item in self.get("items"):
|
||||
validate_account_head(item.idx, item.income_account, self.company, _("Income"))
|
||||
|
||||
def set_tax_withholding(self):
|
||||
if self.get("is_opening") == "Yes":
|
||||
return
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
tax_withholding_account = tax_withholding_details.get("account_head")
|
||||
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_account:
|
||||
d.update(tax_withholding_details)
|
||||
accounts.append(d.account_head)
|
||||
|
||||
if not accounts or tax_withholding_account not in accounts:
|
||||
self.append("taxes", tax_withholding_details)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.taxes
|
||||
if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
# calculate totals again after applying TDS
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def before_save(self):
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.set_paid_amount()
|
||||
@@ -446,8 +466,6 @@ class SalesInvoice(SellingController):
|
||||
# NOTE status updating bypassed for is_return
|
||||
self.status_updater = []
|
||||
|
||||
SalesTaxWithholding(self).on_submit()
|
||||
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
|
||||
@@ -587,7 +605,6 @@ class SalesInvoice(SellingController):
|
||||
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
SalesTaxWithholding(self).on_cancel()
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -629,7 +646,6 @@ class SalesInvoice(SellingController):
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Serial and Batch Bundle",
|
||||
"Tax Withholding Entry",
|
||||
)
|
||||
|
||||
self.delete_auto_created_batches()
|
||||
@@ -1726,7 +1742,7 @@ class SalesInvoice(SellingController):
|
||||
def make_pos_gl_entries(self, gl_entries):
|
||||
if cint(self.is_pos):
|
||||
skip_change_gl_entries = not cint(
|
||||
frappe.get_single_value("POS Settings", "post_change_gl_entries")
|
||||
frappe.get_single_value("Accounts Settings", "post_change_gl_entries")
|
||||
)
|
||||
|
||||
for payment_mode in self.payments:
|
||||
@@ -2310,7 +2326,6 @@ def get_list_context(context=None):
|
||||
"show_search": True,
|
||||
"no_breadcrumbs": True,
|
||||
"title": _("Invoices"),
|
||||
"list_template": "templates/includes/list/list.html",
|
||||
}
|
||||
)
|
||||
return list_context
|
||||
|
||||
@@ -1173,7 +1173,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(expected, res)
|
||||
|
||||
def test_pos_with_no_gl_entry_for_change_amount(self):
|
||||
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 0)
|
||||
frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 0)
|
||||
|
||||
make_pos_profile(
|
||||
company="_Test Company with perpetual inventory",
|
||||
@@ -1221,7 +1221,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
|
||||
|
||||
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
|
||||
frappe.db.set_single_value("Accounts Settings", "post_change_gl_entries", 1)
|
||||
|
||||
def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
|
||||
if validate_without_change_gle:
|
||||
@@ -2510,12 +2510,14 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.submit()
|
||||
|
||||
pda1 = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company",
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company",
|
||||
)
|
||||
)
|
||||
|
||||
pda1.insert()
|
||||
@@ -2566,12 +2568,14 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.submit()
|
||||
|
||||
pda1 = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2019-03-31",
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company",
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2019-03-31",
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company",
|
||||
)
|
||||
)
|
||||
|
||||
pda1.insert()
|
||||
@@ -3474,12 +3478,14 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", getdate("2019-01-31"))
|
||||
|
||||
pda1 = frappe.get_doc(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company",
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company",
|
||||
)
|
||||
)
|
||||
|
||||
pda1.insert()
|
||||
|
||||
@@ -43,14 +43,12 @@
|
||||
"rate",
|
||||
"amount",
|
||||
"item_tax_template",
|
||||
"tax_withholding_category",
|
||||
"col_break3",
|
||||
"base_rate",
|
||||
"base_amount",
|
||||
"pricing_rules",
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"apply_tds",
|
||||
"grant_commission",
|
||||
"section_break_21",
|
||||
"net_rate",
|
||||
@@ -988,21 +986,6 @@
|
||||
"hidden": 1,
|
||||
"label": "SCIO Detail",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"options": "Tax Withholding Category",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -24,7 +24,6 @@ class SalesInvoiceItem(Document):
|
||||
actual_qty: DF.Float
|
||||
allow_zero_valuation_rate: DF.Check
|
||||
amount: DF.Currency
|
||||
apply_tds: DF.Check
|
||||
asset: DF.Link | None
|
||||
barcode: DF.Data | None
|
||||
base_amount: DF.Currency
|
||||
@@ -96,7 +95,6 @@ class SalesInvoiceItem(Document):
|
||||
stock_uom: DF.Link | None
|
||||
stock_uom_rate: DF.Currency
|
||||
target_warehouse: DF.Link | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
total_weight: DF.Float
|
||||
uom: DF.Link
|
||||
use_serial_batch_fields: DF.Check
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"included_in_print_rate",
|
||||
"included_in_paid_amount",
|
||||
"set_by_item_tax_template",
|
||||
"is_tax_withholding_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -203,7 +202,7 @@
|
||||
"fieldname": "dont_recompute_tax",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Don't Recompute Tax",
|
||||
"label": "Dont Recompute tax",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -242,13 +241,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
|
||||
@@ -33,8 +33,6 @@ class SalesTaxesandCharges(Document):
|
||||
dont_recompute_tax: DF.Check
|
||||
included_in_paid_amount: DF.Check
|
||||
included_in_print_rate: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
item_wise_tax_detail: DF.Code | None
|
||||
net_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"read_only": 1,
|
||||
@@ -62,7 +62,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"read_only": 1,
|
||||
@@ -91,16 +91,15 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-10 08:06:40.611761",
|
||||
"modified": "2024-03-27 13:10:39.866399",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Share Balance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class ShareBalance(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
amount: DF.Int
|
||||
current_state: DF.Literal["", "Issued", "Purchased"]
|
||||
from_no: DF.Int
|
||||
is_company: DF.Check
|
||||
@@ -22,7 +22,7 @@ class ShareBalance(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
rate: DF.Currency
|
||||
rate: DF.Int
|
||||
share_type: DF.Link
|
||||
to_no: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nTrialing\nActive\nGrace Period\nCancelled\nUnpaid\nCompleted",
|
||||
"options": "\nTrialing\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -267,7 +267,7 @@
|
||||
"link_fieldname": "subscription"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-23 19:42:52.036034",
|
||||
"modified": "2024-03-27 13:10:47.578120",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
@@ -311,9 +311,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
|
||||
|
||||
class InvoiceCancelled(frappe.ValidationError):
|
||||
@@ -76,7 +77,7 @@ class Subscription(Document):
|
||||
purchase_tax_template: DF.Link | None
|
||||
sales_tax_template: DF.Link | None
|
||||
start_date: DF.Date | None
|
||||
status: DF.Literal["", "Trialing", "Active", "Grace Period", "Cancelled", "Unpaid", "Completed"]
|
||||
status: DF.Literal["", "Trialing", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"]
|
||||
submit_invoice: DF.Check
|
||||
trial_period_end: DF.Date | None
|
||||
trial_period_start: DF.Date | None
|
||||
@@ -222,17 +223,13 @@ class Subscription(Document):
|
||||
"""
|
||||
if self.is_trialling():
|
||||
self.status = "Trialing"
|
||||
elif (
|
||||
not self.has_outstanding_invoice()
|
||||
and self.end_date
|
||||
and getdate(posting_date) > getdate(self.end_date)
|
||||
):
|
||||
elif self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date):
|
||||
self.status = "Completed"
|
||||
elif self.is_past_grace_period():
|
||||
self.status = self.get_status_for_past_grace_period()
|
||||
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||
self.status = "Grace Period"
|
||||
self.status = "Past Due Date"
|
||||
elif not self.has_outstanding_invoice():
|
||||
self.status = "Active"
|
||||
|
||||
@@ -414,10 +411,7 @@ class Subscription(Document):
|
||||
invoice.customer = self.party
|
||||
else:
|
||||
invoice.supplier = self.party
|
||||
tax_withholding_category, tax_withholding_group = frappe.get_cached_value(
|
||||
"Supplier", self.party, ["tax_withholding_category", "tax_withholding_group"]
|
||||
)
|
||||
if tax_withholding_category or tax_withholding_group:
|
||||
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
|
||||
invoice.apply_tds = 1
|
||||
|
||||
# Add currency to invoice
|
||||
@@ -435,6 +429,7 @@ class Subscription(Document):
|
||||
items_list = self.get_items_from_plans(self.plans, is_prorate())
|
||||
|
||||
for item in items_list:
|
||||
item["cost_center"] = self.cost_center
|
||||
invoice.append("items", item)
|
||||
|
||||
# Taxes
|
||||
@@ -566,17 +561,6 @@ class Subscription(Document):
|
||||
self.current_invoice_start, self.current_invoice_end
|
||||
) and self.can_generate_new_invoice(posting_date):
|
||||
self.generate_invoice(posting_date=posting_date)
|
||||
if self.end_date:
|
||||
next_start = add_days(self.current_invoice_end, 1)
|
||||
|
||||
if getdate(next_start) > getdate(self.end_date):
|
||||
if self.cancel_at_period_end:
|
||||
self.cancel_subscription()
|
||||
else:
|
||||
self.set_subscription_status(posting_date=posting_date)
|
||||
|
||||
self.save()
|
||||
return
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
elif posting_date and getdate(posting_date) > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period()
|
||||
|
||||
@@ -17,7 +17,6 @@ from frappe.utils.data import (
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = ("UOM", "Item Group", "Item")
|
||||
@@ -145,17 +144,17 @@ class TestSubscription(IntegrationTestCase):
|
||||
subscription = create_subscription(start_date=add_days(nowdate(), -1000))
|
||||
|
||||
subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice
|
||||
self.assertEqual(subscription.status, "Grace Period")
|
||||
self.assertEqual(subscription.status, "Past Due Date")
|
||||
|
||||
subscription.process()
|
||||
# Grace period is 1000 days so status should remain as Grace Period
|
||||
self.assertEqual(subscription.status, "Grace Period")
|
||||
# Grace period is 1000 days so status should remain as Past Due Date
|
||||
self.assertEqual(subscription.status, "Past Due Date")
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Grace Period")
|
||||
self.assertEqual(subscription.status, "Past Due Date")
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, "Grace Period")
|
||||
self.assertEqual(subscription.status, "Past Due Date")
|
||||
|
||||
settings.grace_period = grace_period
|
||||
settings.save()
|
||||
@@ -584,105 +583,6 @@ class TestSubscription(IntegrationTestCase):
|
||||
subscription.process(nowdate())
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_subscription_auto_cancellation(self):
|
||||
create_plan(
|
||||
plan_name="_Test plan name 10",
|
||||
cost=80,
|
||||
currency="INR",
|
||||
billing_interval="Day",
|
||||
billing_interval_count=3,
|
||||
)
|
||||
start_date = getdate("2025-01-01")
|
||||
subscription = create_subscription(
|
||||
start_date=start_date,
|
||||
end_date=add_days(start_date, 8),
|
||||
cancel_at_period_end=1,
|
||||
generate_new_invoices_past_due_date=1,
|
||||
generate_invoice_at="Beginning of the current subscription period",
|
||||
plans=[{"plan": "_Test plan name 10", "qty": 1}],
|
||||
)
|
||||
subscription.process(posting_date=add_days(start_date, 2))
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
subscription.process(posting_date=add_days(start_date, 5))
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
|
||||
subscription.process(posting_date=add_days(start_date, 8))
|
||||
self.assertEqual(len(subscription.invoices), 3)
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
|
||||
def test_subscription_auto_cancellation_uneven_cycle(self):
|
||||
create_plan(
|
||||
plan_name="_Test plan name 10",
|
||||
cost=80,
|
||||
currency="INR",
|
||||
billing_interval="Day",
|
||||
billing_interval_count=3,
|
||||
)
|
||||
start_date = getdate("2025-01-01")
|
||||
subscription = create_subscription(
|
||||
start_date=start_date,
|
||||
end_date=add_days(start_date, 6),
|
||||
cancel_at_period_end=1,
|
||||
generate_new_invoices_past_due_date=1,
|
||||
generate_invoice_at="Beginning of the current subscription period",
|
||||
plans=[{"plan": "_Test plan name 10", "qty": 1}],
|
||||
)
|
||||
|
||||
subscription.process(posting_date=add_days(start_date, 2))
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
subscription.process(posting_date=add_days(start_date, 5))
|
||||
self.assertEqual(len(subscription.invoices), 2)
|
||||
|
||||
# partial last cycle invoice
|
||||
subscription.process(posting_date=add_days(start_date, 6))
|
||||
self.assertEqual(len(subscription.invoices), 3)
|
||||
|
||||
self.assertEqual(subscription.status, "Cancelled")
|
||||
|
||||
self.assertRaises(frappe.ValidationError, subscription.process, posting_date=add_days(start_date, 7))
|
||||
|
||||
def test_subscription_auto_completion(self):
|
||||
create_plan(
|
||||
plan_name="_Test Plan 3 Day",
|
||||
cost=100,
|
||||
billing_interval="Day",
|
||||
billing_interval_count=3,
|
||||
currency="INR",
|
||||
)
|
||||
|
||||
start_date = getdate("2025-01-01")
|
||||
end_date = add_days(start_date, 6)
|
||||
|
||||
subscription = create_subscription(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
party_type="Customer",
|
||||
party="_Test Customer",
|
||||
generate_invoice_at="Beginning of the current subscription period",
|
||||
generate_new_invoices_past_due_date=1,
|
||||
plans=[{"plan": "_Test Plan 3 Day", "qty": 1}],
|
||||
)
|
||||
|
||||
for day in range(0, 10):
|
||||
if subscription.status == "Cancelled":
|
||||
break
|
||||
subscription.process(posting_date=add_days(start_date, day))
|
||||
|
||||
invoices = frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={"subscription": subscription.name, "docstatus": 1},
|
||||
fields=["name", "from_date", "to_date"],
|
||||
order_by="from_date asc",
|
||||
)
|
||||
for invoice in invoices:
|
||||
pi = get_payment_entry("Sales Invoice", invoice.name)
|
||||
pi.submit()
|
||||
# After processing through all days, subscription should be completed
|
||||
subscription.process(posting_date=add_days(end_date, 1))
|
||||
self.assertEqual(subscription.status, "Completed")
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
@@ -753,13 +653,12 @@ def reset_settings():
|
||||
|
||||
def create_subscription(**kwargs):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = kwargs.get("party_type") or "Customer"
|
||||
subscription.party_type = (kwargs.get("party_type") or "Customer",)
|
||||
subscription.company = kwargs.get("company") or "_Test Company"
|
||||
subscription.party = kwargs.get("party") or "_Test Customer"
|
||||
subscription.trial_period_start = kwargs.get("trial_period_start")
|
||||
subscription.trial_period_end = kwargs.get("trial_period_end")
|
||||
subscription.start_date = kwargs.get("start_date")
|
||||
subscription.end_date = kwargs.get("end_date")
|
||||
subscription.generate_invoice_at = kwargs.get("generate_invoice_at")
|
||||
subscription.additional_discount_percentage = kwargs.get("additional_discount_percentage")
|
||||
subscription.additional_discount_amount = kwargs.get("additional_discount_amount")
|
||||
@@ -768,7 +667,6 @@ def create_subscription(**kwargs):
|
||||
subscription.submit_invoice = kwargs.get("submit_invoice")
|
||||
subscription.days_until_due = kwargs.get("days_until_due")
|
||||
subscription.number_of_days = kwargs.get("number_of_days")
|
||||
subscription.cancel_at_period_end = kwargs.get("cancel_at_period_end")
|
||||
|
||||
if not kwargs.get("plans"):
|
||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||
|
||||
@@ -30,11 +30,9 @@
|
||||
"label": "Prorate"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:34.671062",
|
||||
"modified": "2024-03-27 13:10:48.283833",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Settings",
|
||||
@@ -72,9 +70,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user