mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-29 23:43:40 +00:00
Compare commits
181 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b494dc9e8 | ||
|
|
ed69dafbe8 | ||
|
|
c985f94009 | ||
|
|
0f27881fed | ||
|
|
e60490dceb | ||
|
|
2cd4c1a052 | ||
|
|
982810a700 | ||
|
|
18006b978f | ||
|
|
bbb4e79d0a | ||
|
|
bca893a508 | ||
|
|
0dade2c38c | ||
|
|
a22d773341 | ||
|
|
126e13be25 | ||
|
|
288cdf3bf0 | ||
|
|
2422237c1a | ||
|
|
38cfeb1bb7 | ||
|
|
d27cf48b19 | ||
|
|
c232f1f450 | ||
|
|
bd932da08b | ||
|
|
07a957c164 | ||
|
|
d3c893d08b | ||
|
|
b3001595ab | ||
|
|
86cf256358 | ||
|
|
19a8ebe8a5 | ||
|
|
6dbc17d71a | ||
|
|
7bd360aa29 | ||
|
|
2e438011da | ||
|
|
48ebb4ca61 | ||
|
|
808214fd95 | ||
|
|
d6f2ff6b87 | ||
|
|
9db03bc520 | ||
|
|
e24ab72c0d | ||
|
|
4d5c665e22 | ||
|
|
e09487d140 | ||
|
|
5de4b013ea | ||
|
|
8f8bf13b41 | ||
|
|
11117710d3 | ||
|
|
90b07b3db5 | ||
|
|
0d498baa10 | ||
|
|
deef1696d6 | ||
|
|
c6ee18b4d4 | ||
|
|
2f88fa6731 | ||
|
|
9c5c87b354 | ||
|
|
64a724baea | ||
|
|
7f32c3aca7 | ||
|
|
4675921077 | ||
|
|
d51ce66cb2 | ||
|
|
6bd6e62c8c | ||
|
|
fa901946ce | ||
|
|
446c111653 | ||
|
|
859b24dd95 | ||
|
|
7b60ec8457 | ||
|
|
e6f0bb66e2 | ||
|
|
6570796fba | ||
|
|
fba78711cc | ||
|
|
9f04fcc190 | ||
|
|
5289aa0ab3 | ||
|
|
185ef4e273 | ||
|
|
45dc2c40fd | ||
|
|
386a373c9b | ||
|
|
134e4b7446 | ||
|
|
15b6633fc3 | ||
|
|
f14751d538 | ||
|
|
f7fa394aea | ||
|
|
d9a9a5bcde | ||
|
|
0df38a841e | ||
|
|
d56df96f73 | ||
|
|
cc85370d54 | ||
|
|
ac9aa7f154 | ||
|
|
23cac0df83 | ||
|
|
5c0d2cb474 | ||
|
|
cabea2f288 | ||
|
|
4c95daaca5 | ||
|
|
62bbe28a72 | ||
|
|
85d1eb8379 | ||
|
|
8de9ac4e34 | ||
|
|
f3996fb971 | ||
|
|
764c775e19 | ||
|
|
66ec6a4d20 | ||
|
|
610735d1c5 | ||
|
|
8a5fa64e1d | ||
|
|
aecf2c1c0e | ||
|
|
719d982a07 | ||
|
|
1b146738c4 | ||
|
|
c4010b0581 | ||
|
|
456e99b352 | ||
|
|
2a244d162b | ||
|
|
ddc9ea16cc | ||
|
|
f7b87ed0e3 | ||
|
|
01b22254e7 | ||
|
|
df3fbeded2 | ||
|
|
96bab08ae0 | ||
|
|
055ff56ce4 | ||
|
|
4f8184ec70 | ||
|
|
c6d4802857 | ||
|
|
6392126ca5 | ||
|
|
40466be9ef | ||
|
|
e6cfdb8e4d | ||
|
|
fffaf834fd | ||
|
|
be0e58fb23 | ||
|
|
9276cd7343 | ||
|
|
dd2763aabc | ||
|
|
386f49978e | ||
|
|
090aab33fb | ||
|
|
99bc2c174b | ||
|
|
91a748d9bf | ||
|
|
c99b9e1b64 | ||
|
|
a9747213f5 | ||
|
|
5923618df3 | ||
|
|
193a44f298 | ||
|
|
d9731d7c72 | ||
|
|
457adcee95 | ||
|
|
01aff6492c | ||
|
|
57cd2a06e8 | ||
|
|
62a9a761b7 | ||
|
|
9e6300bf76 | ||
|
|
5397b7da25 | ||
|
|
37d080bdb4 | ||
|
|
aa359aded4 | ||
|
|
27c5dab7e4 | ||
|
|
4e05277695 | ||
|
|
319d769c6f | ||
|
|
78497336c7 | ||
|
|
d5ea039e07 | ||
|
|
4bb30a9157 | ||
|
|
dd6d4d1910 | ||
|
|
2c73e37f80 | ||
|
|
3bee79b90d | ||
|
|
47abaf70b2 | ||
|
|
4940aeb712 | ||
|
|
95213fb9b8 | ||
|
|
a7b1fec21d | ||
|
|
534891aac4 | ||
|
|
0d6d64ff05 | ||
|
|
fa76e8ac7f | ||
|
|
abed348121 | ||
|
|
b1825c0cbe | ||
|
|
cfcba1fcf2 | ||
|
|
d542a72da5 | ||
|
|
aa2cba9780 | ||
|
|
cac907383b | ||
|
|
7556550158 | ||
|
|
78aaf6c7e8 | ||
|
|
104eac21e8 | ||
|
|
3914d5d1b7 | ||
|
|
52a4ca9c41 | ||
|
|
de66fd0c58 | ||
|
|
450b4c2f5f | ||
|
|
d2cc549696 | ||
|
|
493f36b3ce | ||
|
|
cd605d35c5 | ||
|
|
5916e570af | ||
|
|
4d300f7d34 | ||
|
|
36cc39ddc6 | ||
|
|
afd25508d6 | ||
|
|
46f5de0b1c | ||
|
|
b252ad49b7 | ||
|
|
10dbfd310f | ||
|
|
07bcaab33b | ||
|
|
9d969d5af5 | ||
|
|
2e7c4776d4 | ||
|
|
5a2933df8f | ||
|
|
fa5e4dee17 | ||
|
|
3ba400a02b | ||
|
|
5c064331cb | ||
|
|
151864079b | ||
|
|
f785f36ad6 | ||
|
|
d49c34389b | ||
|
|
ead7744f81 | ||
|
|
b487f69b59 | ||
|
|
d74e632934 | ||
|
|
29ba701432 | ||
|
|
f287edd8c2 | ||
|
|
d506e574d2 | ||
|
|
45052ce8a7 | ||
|
|
4f9f90738a | ||
|
|
101f68c8e8 | ||
|
|
ffebb86846 | ||
|
|
6cc560a579 | ||
|
|
47e78bd4b9 | ||
|
|
d7da5b047d |
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@@ -41,6 +41,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
TZ: 'Asia/Kolkata'
|
||||
NODE_ENV: "production"
|
||||
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
mysql:
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
TZ: 'Asia/Kolkata'
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.14.0"
|
||||
__version__ = "16.17.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -5,8 +5,7 @@ frappe.ui.form.on("Account", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("parent_account", "report_type", "report_type");
|
||||
frm.add_fetch("parent_account", "root_type", "root_type");
|
||||
},
|
||||
onload: function (frm) {
|
||||
|
||||
frm.set_query("parent_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -15,7 +14,18 @@ frappe.ui.form.on("Account", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account_category", function () {
|
||||
if (!frm.doc.root_type) return;
|
||||
|
||||
return {
|
||||
filters: {
|
||||
root_type: ["in", [frm.doc.root_type, ""]],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.toggle_display("account_name", frm.is_new());
|
||||
|
||||
@@ -58,12 +68,20 @@ frappe.ui.form.on("Account", {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
account_type: function (frm) {
|
||||
if (frm.doc.is_group == 0) {
|
||||
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
|
||||
frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
|
||||
}
|
||||
},
|
||||
|
||||
root_type: function (frm) {
|
||||
if (frm.doc.account_category) {
|
||||
frm.set_value("account_category", "");
|
||||
}
|
||||
},
|
||||
|
||||
add_toolbar_buttons: function (frm) {
|
||||
frm.add_custom_button(
|
||||
__("Chart of Accounts"),
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-02 06:26:44.657146",
|
||||
"modified": "2026-04-14 18:14:42.202065",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -256,6 +256,14 @@
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "HR User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
|
||||
@@ -34,6 +34,13 @@
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Anlagen im Bau": {
|
||||
"is_group": 1,
|
||||
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
|
||||
"account_number": "0498",
|
||||
"account_type": "Capital Work in Progress"
|
||||
}
|
||||
},
|
||||
"Accumulated Depreciation": {
|
||||
"account_type": "Accumulated Depreciation"
|
||||
}
|
||||
@@ -317,13 +324,21 @@
|
||||
"account_number": "3800",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
|
||||
"account_number": "3960",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Herstellungskosten": {
|
||||
"account_number": "4996",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
|
||||
"account_number": "2310",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
|
||||
"account_number": "2320",
|
||||
"account_type": "Stock Adjustment"
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verwaltungskosten": {
|
||||
"account_number": "4997",
|
||||
@@ -340,7 +355,7 @@
|
||||
"is_group": 1,
|
||||
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
|
||||
"account_number": "4830",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Gebäude": {
|
||||
"account_number": "4831",
|
||||
|
||||
@@ -321,72 +321,6 @@ class TestAccount(ERPNextTestSuite):
|
||||
self.assertEqual(balance, 0)
|
||||
|
||||
|
||||
def _make_test_records(verbose=None):
|
||||
from frappe.tests.utils import make_test_objects
|
||||
|
||||
accounts = [
|
||||
# [account_name, parent_account, is_group]
|
||||
["_Test Bank", "Bank Accounts", 0, "Bank", None],
|
||||
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
|
||||
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
|
||||
["_Test Cash", "Cash In Hand", 0, "Cash", None],
|
||||
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
|
||||
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
|
||||
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
|
||||
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
|
||||
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
|
||||
["_Test Employee Advance", "Current Liabilities", 0, None, None],
|
||||
["_Test Account Tax Assets", "Current Assets", 1, None, None],
|
||||
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
|
||||
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
|
||||
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
["_Test Account S&H Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
|
||||
["_Test Account CST", "Direct Expenses", 0, "Tax", None],
|
||||
["_Test Account Discount", "Direct Expenses", 0, None, None],
|
||||
["_Test Write Off", "Indirect Expenses", 0, None, None],
|
||||
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
|
||||
["_Test Account Sales", "Direct Income", 0, None, None],
|
||||
# related to Account Inventory Integration
|
||||
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
|
||||
# fixed asset depreciation
|
||||
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
|
||||
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
|
||||
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
|
||||
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
|
||||
# Receivable / Payable Account
|
||||
["_Test Receivable", "Current Assets", 0, "Receivable", None],
|
||||
["_Test Payable", "Current Liabilities", 0, "Payable", None],
|
||||
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
|
||||
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
|
||||
]
|
||||
|
||||
for company, abbr in [
|
||||
["_Test Company", "_TC"],
|
||||
["_Test Company 1", "_TC1"],
|
||||
["_Test Company with perpetual inventory", "TCP1"],
|
||||
]:
|
||||
test_objects = make_test_objects(
|
||||
"Account",
|
||||
[
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": account_name,
|
||||
"parent_account": parent_account + " - " + abbr,
|
||||
"company": company,
|
||||
"is_group": is_group,
|
||||
"account_type": account_type,
|
||||
"account_currency": currency,
|
||||
}
|
||||
for account_name, parent_account, is_group, account_type, currency in accounts
|
||||
],
|
||||
)
|
||||
|
||||
return test_objects
|
||||
|
||||
|
||||
def get_inventory_account(company, warehouse=None):
|
||||
account = None
|
||||
if warehouse:
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_category_name",
|
||||
"root_type",
|
||||
"column_break_qluu",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
@@ -14,6 +16,7 @@
|
||||
"fieldname": "account_category_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Category Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
@@ -22,6 +25,18 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qluu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "root_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Root Type",
|
||||
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -32,7 +47,7 @@
|
||||
"link_fieldname": "account_category"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-23 01:19:49.589393",
|
||||
"modified": "2026-03-05 06:49:34.430723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Category",
|
||||
@@ -69,7 +84,7 @@
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_category_name, description",
|
||||
"search_fields": "account_category_name, root_type",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
|
||||
@@ -21,6 +21,7 @@ class AccountCategory(Document):
|
||||
|
||||
account_category_name: DF.Data
|
||||
description: DF.SmallText | None
|
||||
root_type: DF.Literal["", "Asset", "Liability", "Income", "Expense", "Equity"]
|
||||
# end: auto-generated types
|
||||
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
|
||||
@@ -82,7 +82,7 @@ class AccountingDimension(Document):
|
||||
else:
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def after_insert(self):
|
||||
def on_update(self):
|
||||
if frappe.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {},
|
||||
refresh: function (frm) {
|
||||
frm.set_query("document_type", "repost_allowed_types", function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", frappe.boot.sysdefaults.repost_allowed_doctypes],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
enable_immutable_ledger: function (frm) {
|
||||
if (!frm.doc.enable_immutable_ledger) {
|
||||
return;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"invoicing_features_section",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"automatically_fetch_payment_terms",
|
||||
"enable_subscription",
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
@@ -62,6 +63,8 @@
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"repost_section",
|
||||
"repost_allowed_types",
|
||||
"payment_options_section",
|
||||
"enable_loyalty_point_program",
|
||||
"column_break_ctam",
|
||||
@@ -702,6 +705,17 @@
|
||||
"fieldname": "fetch_payment_schedule_in_payment_request",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Payment Schedule In Payment Request"
|
||||
},
|
||||
{
|
||||
"fieldname": "repost_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Repost"
|
||||
},
|
||||
{
|
||||
"fieldname": "repost_allowed_types",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allowed Doctypes",
|
||||
"options": "Repost Allowed Types"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -711,7 +725,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-30 07:32:58.182018",
|
||||
"modified": "2026-04-13 15:30:28.729627",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -10,6 +10,9 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
|
||||
SELLING_DOCTYPES = [
|
||||
@@ -44,6 +47,8 @@ class AccountsSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
|
||||
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
@@ -72,6 +77,7 @@ class AccountsSettings(Document):
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_loyalty_point_program: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
enable_subscription: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_payment_schedule_in_payment_request: DF.Check
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
@@ -86,6 +92,7 @@ class AccountsSettings(Document):
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
repost_allowed_types: DF.Table[RepostAllowedTypes]
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_notify_on_depreciation_failure: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
@@ -136,10 +143,15 @@ class AccountsSettings(Document):
|
||||
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
|
||||
clear_cache = True
|
||||
|
||||
if old_doc.enable_subscription != self.enable_subscription:
|
||||
toggle_subscription_sections(not self.enable_subscription)
|
||||
clear_cache = True
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
self.validate_and_sync_auto_reconcile_config()
|
||||
self.update_property_for_accounting_dimension()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
@@ -186,6 +198,17 @@ class AccountsSettings(Document):
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
def update_property_for_accounting_dimension(self):
|
||||
doctypes = [entry.document_type for entry in self.repost_allowed_types]
|
||||
if not doctypes:
|
||||
return
|
||||
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
|
||||
|
||||
doctypes += get_child_docs(doctypes)
|
||||
|
||||
set_allow_on_submit_for_dimension_fields(doctypes)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
@@ -216,6 +239,12 @@ def toggle_loyalty_point_program_section(hide):
|
||||
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
|
||||
|
||||
|
||||
def toggle_subscription_sections(hide):
|
||||
subscription_doctypes = frappe.get_hooks("subscription_doctypes")
|
||||
for doctype in subscription_doctypes:
|
||||
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
|
||||
|
||||
|
||||
def create_property_setter_for_hiding_field(doctype, field_name, hide):
|
||||
make_property_setter(
|
||||
doctype,
|
||||
@@ -225,3 +254,12 @@ def create_property_setter_for_hiding_field(doctype, field_name, hide):
|
||||
"Check",
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
|
||||
def set_allow_on_submit_for_dimension_fields(doctypes):
|
||||
for dt in doctypes:
|
||||
meta = frappe.get_meta(dt)
|
||||
for dimension in get_accounting_dimensions():
|
||||
df = meta.get_field(dimension)
|
||||
if df and not df.allow_on_submit:
|
||||
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-22 10:46:42.904001",
|
||||
"modified": "2026-04-14 18:15:27.367298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center",
|
||||
@@ -173,11 +173,20 @@
|
||||
"role": "Employee",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"role": "HR User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "parent_cost_center, is_group",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import json
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from functools import reduce
|
||||
from functools import cache, reduce
|
||||
from typing import Any, Union
|
||||
|
||||
import frappe
|
||||
@@ -15,6 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cstr, date_diff, flt, getdate
|
||||
from frappe.utils.xlsxutils import XLSXMetadata, XLSXStyleBuilder
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
|
||||
from erpnext import get_company_currency
|
||||
@@ -38,6 +39,9 @@ from erpnext.accounts.report.financial_statements import (
|
||||
)
|
||||
from erpnext.accounts.utils import get_children, get_currency_precision
|
||||
|
||||
DEFAULT_BULLET_PREFIX = "• "
|
||||
SEGMENT_PREFIX = "seg_"
|
||||
|
||||
# ============================================================================
|
||||
# DATA MODELS
|
||||
# ============================================================================
|
||||
@@ -141,7 +145,7 @@ class SegmentData:
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return f"seg_{self.index}"
|
||||
return f"{SEGMENT_PREFIX}{self.index}"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -222,14 +226,38 @@ class FinancialReportEngine:
|
||||
return context.get_result()
|
||||
|
||||
def _validate_filters(self, filters: dict[str, Any]) -> None:
|
||||
required_filters = ["report_template", "period_start_date", "period_end_date"]
|
||||
filter_labels = {
|
||||
"report_template": _("Report Template"),
|
||||
"filter_based_on": _("Filter Based On"),
|
||||
"period_start_date": _("Start Date"),
|
||||
"period_end_date": _("End Date"),
|
||||
"from_fiscal_year": _("Start Year"),
|
||||
"to_fiscal_year": _("End Year"),
|
||||
}
|
||||
|
||||
required_filters_by_basis = {
|
||||
"Date Range": ("period_start_date", "period_end_date"),
|
||||
"Fiscal Year": ("from_fiscal_year", "to_fiscal_year"),
|
||||
}
|
||||
|
||||
required_filters = ["report_template", "filter_based_on"]
|
||||
required_filters.extend(required_filters_by_basis.get(filters.get("filter_based_on"), ()))
|
||||
|
||||
for filter_key in required_filters:
|
||||
if not filters.get(filter_key):
|
||||
frappe.throw(_("Missing required filter: {0}").format(filter_key))
|
||||
frappe.throw(
|
||||
title=_("Missing Required Filter"),
|
||||
msg=_("Missing required filter: {0}").format(
|
||||
frappe.bold(filter_labels.get(filter_key, filter_key))
|
||||
),
|
||||
)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
frappe.msgprint(_("Currency filters are currently unsupported in Custom Financial Report."))
|
||||
frappe.msgprint(
|
||||
title=_("Unsupported Feature"),
|
||||
msg=_("Currency filters are currently unsupported in Custom Financial Report."),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
# Margin view is dependent on first row being an income account. Hence not supported.
|
||||
# Way to implement this would be using calculated rows with formulas.
|
||||
@@ -464,6 +492,7 @@ class FinancialQueryBuilder:
|
||||
self.periods = periods
|
||||
self.company = filters.get("company")
|
||||
self.account_meta = {} # {name: {account_name, account_number}}
|
||||
self.ignore_opening_entries = False
|
||||
|
||||
def fetch_account_balances(self, accounts: list[dict]) -> dict[str, AccountData]:
|
||||
"""
|
||||
@@ -501,6 +530,8 @@ class FinancialQueryBuilder:
|
||||
"""
|
||||
Return opening balances for *all accounts* defaulting to zero.
|
||||
"""
|
||||
self.ignore_opening_entries = False
|
||||
|
||||
if frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance"):
|
||||
return self._get_opening_balances_from_gl(accounts)
|
||||
|
||||
@@ -520,9 +551,9 @@ class FinancialQueryBuilder:
|
||||
if last_closing_voucher:
|
||||
closing_voucher = last_closing_voucher[0]
|
||||
closing_data = self._get_closing_balances(accounts, closing_voucher.name)
|
||||
self.ignore_opening_entries = True # Else it will double count
|
||||
|
||||
if sum(closing_data.values()) != 0.0:
|
||||
return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date)
|
||||
return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date)
|
||||
|
||||
return self._get_opening_balances_from_gl(accounts)
|
||||
|
||||
@@ -616,7 +647,12 @@ class FinancialQueryBuilder:
|
||||
.groupby(gl_table.account)
|
||||
)
|
||||
|
||||
if not frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting"):
|
||||
ignore_is_opening = frappe.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
if self.ignore_opening_entries and not ignore_is_opening:
|
||||
# This filter here applies to all accounts (BS & PL)
|
||||
# However, in legacy query, this filter only applies to BS accounts
|
||||
query = query.where(gl_table.is_opening == "No")
|
||||
|
||||
# Add period-specific columns
|
||||
@@ -680,11 +716,18 @@ class FinancialQueryBuilder:
|
||||
account_data.unaccumulate_values()
|
||||
|
||||
def _apply_standard_filters(self, query, table, doctype: str = "GL Entry"):
|
||||
if self.filters.get("ignore_closing_entries"):
|
||||
if doctype == "GL Entry":
|
||||
query = query.where(table.voucher_type != "Period Closing Voucher")
|
||||
else:
|
||||
query = query.where(table.is_period_closing_voucher_entry == 0)
|
||||
# Exclude PCV-generated entries except those posted to a closing-account-head
|
||||
# so BS retained earnings survive while P&L reversal entries are filtered out
|
||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||
closing_heads = frappe.qb.from_(pcv).select(pcv.closing_account_head).where(pcv.docstatus == 1)
|
||||
|
||||
if doctype == "GL Entry":
|
||||
is_pcv = table.voucher_type == "Period Closing Voucher"
|
||||
else:
|
||||
# Account Closing Balance
|
||||
is_pcv = table.is_period_closing_voucher_entry == 1
|
||||
|
||||
query = query.where(~is_pcv | table.account.isin(closing_heads))
|
||||
|
||||
if self.filters.get("project"):
|
||||
projects = self.filters.get("project")
|
||||
@@ -1392,7 +1435,8 @@ class FormattingEngine:
|
||||
condition=lambda rd: getattr(rd.row, "italic_text", False), format_properties={"italic": True}
|
||||
),
|
||||
FormattingRule(
|
||||
condition=lambda rd: rd.is_detail_row, format_properties={"is_detail": True, "prefix": "• "}
|
||||
condition=lambda rd: rd.is_detail_row,
|
||||
format_properties={"is_detail": True, "prefix": DEFAULT_BULLET_PREFIX},
|
||||
),
|
||||
FormattingRule(
|
||||
condition=lambda rd: getattr(rd.row, "warn_if_negative", False),
|
||||
@@ -1838,3 +1882,124 @@ class GrowthViewTransformer:
|
||||
return 0.0
|
||||
else:
|
||||
return flt(((current_value - previous_value) / abs(previous_value)) * 100, 2)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# XLSX EXPORT STYLING
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_xlsx_styles(metadata: XLSXMetadata) -> dict | None:
|
||||
"""
|
||||
Generate XLSX styles for financial report templates.
|
||||
|
||||
NOTE: Currently only custom report generated with "Report Template" filter will have styles applied.
|
||||
"""
|
||||
# skip styling
|
||||
if not metadata.filters.get("report_template"):
|
||||
return
|
||||
|
||||
builder = XLSXStyleBuilder(metadata, default_styling=False)
|
||||
builder.apply_default_styles(currency_formatting=False)
|
||||
|
||||
# currency is fixed for all columns (only if report template filter is applied)
|
||||
currency = get_company_currency(metadata.filters.get("company"))
|
||||
|
||||
styles = {
|
||||
"bold": builder.register_style({"bold": True}),
|
||||
"italic": builder.register_style({"italic": True}),
|
||||
"warning": builder.register_style({"font_color": "#dc3545"}), # text-danger
|
||||
}
|
||||
|
||||
fieldtype_formats = {
|
||||
"Int": builder.register_style({"num_format": "General"}),
|
||||
"Float": builder.register_style({"num_format": builder.get_number_format("Float")}),
|
||||
"Percent": builder.register_style({"num_format": builder.get_number_format("Percent")}),
|
||||
"Currency": builder.register_style({"num_format": builder.get_number_format("Currency", currency)}),
|
||||
}
|
||||
|
||||
# quick access for hot loop
|
||||
style_cell = builder.style_cell
|
||||
|
||||
@cache
|
||||
def get_color_style(color: str) -> int:
|
||||
return builder.register_style({"font_color": color})
|
||||
|
||||
@cache
|
||||
def get_prefix_style(prefix: str) -> int:
|
||||
prefix = f"{prefix or DEFAULT_BULLET_PREFIX}@"
|
||||
|
||||
return builder.register_style({"num_format": prefix})
|
||||
|
||||
@cache
|
||||
def get_indent_style(indent: int) -> int:
|
||||
return builder.register_style({"align": "left", "indent": indent})
|
||||
|
||||
# column level styling of currency columns
|
||||
for col_idx, col in metadata.column_map.items():
|
||||
if col.get("fieldtype") != "Currency":
|
||||
continue
|
||||
|
||||
builder.style_column(col_idx, fieldtype_formats["Currency"])
|
||||
|
||||
# cell level styling
|
||||
for row_idx, row in metadata.row_map.items():
|
||||
# skip total row
|
||||
if metadata.has_total_row and row_idx == builder.last_row_index:
|
||||
continue
|
||||
|
||||
is_segmented = (row.get("_segment_info", {}).get("total_segments", 1) or 1) > 1
|
||||
segment_values = row.get("segment_values", {}) or {}
|
||||
|
||||
for col_idx, col in metadata.column_map.items():
|
||||
fieldname = col.get("fieldname")
|
||||
is_account = fieldname == "account"
|
||||
|
||||
# determine formatting bucket
|
||||
if is_segmented and fieldname.startswith(SEGMENT_PREFIX):
|
||||
formatting = row.copy()
|
||||
|
||||
_, seg_idx, seg_fieldname = fieldname.split("_", 2)
|
||||
is_account = seg_fieldname == "account"
|
||||
formatting.update(segment_values.get(f"{SEGMENT_PREFIX}{seg_idx}", {}) or {})
|
||||
else:
|
||||
formatting = row # default formatting bucket.
|
||||
|
||||
if not is_account and formatting.get("is_blank_line"):
|
||||
continue
|
||||
|
||||
col_fieldtype = col.get("fieldtype")
|
||||
cell_fieldtype = formatting.get("fieldtype") or col_fieldtype
|
||||
cell_value = row.get(fieldname)
|
||||
|
||||
if cell_value in (None, ""):
|
||||
continue
|
||||
|
||||
# account column and other fieldtype styling
|
||||
if is_account:
|
||||
if formatting.get("is_detail") or (prefix := formatting.get("prefix")):
|
||||
style_cell(row_idx, col_idx, get_prefix_style(prefix))
|
||||
|
||||
# custom indentation (different segment might have different indentation levels)
|
||||
if is_segmented and (indent := formatting.get("indent")) and indent > 0:
|
||||
style_cell(row_idx, col_idx, get_indent_style(indent))
|
||||
else:
|
||||
if col_fieldtype != cell_fieldtype and cell_fieldtype in fieldtype_formats:
|
||||
style_cell(row_idx, col_idx, fieldtype_formats[cell_fieldtype])
|
||||
|
||||
# text styles
|
||||
for style_key in ("bold", "italic"):
|
||||
if formatting.get(style_key):
|
||||
style_cell(row_idx, col_idx, styles[style_key])
|
||||
|
||||
# color styles
|
||||
if (
|
||||
formatting.get("warn_if_negative")
|
||||
and cell_fieldtype in frappe.model.numeric_fieldtypes
|
||||
and flt(cell_value) < 0
|
||||
):
|
||||
style_cell(row_idx, col_idx, styles["warning"])
|
||||
elif color := formatting.get("color"):
|
||||
style_cell(row_idx, col_idx, get_color_style(color))
|
||||
|
||||
return builder.result
|
||||
|
||||
@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine
|
||||
DependencyResolver,
|
||||
FilterExpressionParser,
|
||||
FinancialQueryBuilder,
|
||||
FinancialReportEngine,
|
||||
FormulaCalculator,
|
||||
PeriodValue,
|
||||
)
|
||||
@@ -1952,6 +1953,159 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
|
||||
|
||||
jv_2023.cancel()
|
||||
|
||||
def test_opening_entries_roll_into_opening_after_period_closing(self):
|
||||
"""
|
||||
Sequence:
|
||||
1. is_opening JV of 3000 in current year (FY 2024)
|
||||
2. is_opening JV of 5000 in next year (FY 2025)
|
||||
3. Period Closing Voucher for previous year (FY 2023)
|
||||
|
||||
Expected (BS report for FY 2024):
|
||||
opening of FY 2024 = 3000 + 5000 = 8000
|
||||
(all is_opening entries roll into opening irrespective of fiscal year,
|
||||
on top of the PCV carry-forward — here PCV closing for cash is 0).
|
||||
"""
|
||||
company = "_Test Company"
|
||||
cash_account = "_Test Cash - _TC"
|
||||
# Opening JVs cannot post against P&L accounts; use a Balance Sheet offset.
|
||||
opening_offset_account = "Temporary Opening - _TC"
|
||||
|
||||
pcv = None
|
||||
jv_current_year = None
|
||||
jv_next_year = None
|
||||
original_pcv_setting = frappe.db.get_single_value(
|
||||
"Accounts Settings", "use_legacy_controller_for_pcv"
|
||||
)
|
||||
|
||||
try:
|
||||
# Step 1: opening JV in current year (FY 2024) — must be posted before PCV
|
||||
# exists, else `validate_against_pcv` rejects it.
|
||||
jv_current_year = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=opening_offset_account,
|
||||
amount=3000,
|
||||
posting_date="2024-06-15",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv_current_year.is_opening = "Yes"
|
||||
jv_current_year.insert()
|
||||
jv_current_year.submit()
|
||||
|
||||
# Step 2: opening JV in next year (FY 2025)
|
||||
jv_next_year = make_journal_entry(
|
||||
account1=cash_account,
|
||||
account2=opening_offset_account,
|
||||
amount=5000,
|
||||
posting_date="2025-06-15",
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv_next_year.is_opening = "Yes"
|
||||
jv_next_year.insert()
|
||||
jv_next_year.submit()
|
||||
|
||||
# Step 3: book Period Closing Voucher for previous year (FY 2023)
|
||||
closing_account = frappe.db.get_value(
|
||||
"Account",
|
||||
{
|
||||
"company": company,
|
||||
"root_type": "Liability",
|
||||
"is_group": 0,
|
||||
"account_type": ["not in", ["Payable", "Receivable"]],
|
||||
},
|
||||
"name",
|
||||
)
|
||||
fy_2023 = get_fiscal_year("2023-06-15", company=company)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": "2023-12-31",
|
||||
"period_start_date": fy_2023[1],
|
||||
"period_end_date": fy_2023[2],
|
||||
"company": company,
|
||||
"fiscal_year": fy_2023[0],
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"closing_account_head": closing_account,
|
||||
"remarks": "Test Period Closing",
|
||||
}
|
||||
)
|
||||
pcv.insert()
|
||||
pcv.submit()
|
||||
pcv.reload()
|
||||
|
||||
# Run BS report for FY 2024
|
||||
filters = {
|
||||
"company": company,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-01-01",
|
||||
"period_end_date": "2024-12-31",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Yearly",
|
||||
"ignore_closing_entries": True,
|
||||
}
|
||||
|
||||
periods = [{"key": "2024", "from_date": "2024-01-01", "to_date": "2024-12-31"}]
|
||||
|
||||
query_builder = FinancialQueryBuilder(filters, periods)
|
||||
accounts = [
|
||||
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||
frappe._dict(
|
||||
{
|
||||
"name": opening_offset_account,
|
||||
"account_name": "Temporary Opening",
|
||||
"account_number": "1900",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
balances_data = query_builder.fetch_account_balances(accounts)
|
||||
cash_data = balances_data.get(cash_account)
|
||||
offset_data = balances_data.get(opening_offset_account)
|
||||
self.assertIsNotNone(cash_data, "Cash account should exist in results")
|
||||
self.assertIsNotNone(offset_data, "Offset account should exist in results")
|
||||
|
||||
year_2024_cash = cash_data.get_period("2024")
|
||||
year_2024_offset = offset_data.get_period("2024")
|
||||
self.assertIsNotNone(year_2024_cash, "FY 2024 period should exist for cash")
|
||||
self.assertIsNotNone(year_2024_offset, "FY 2024 period should exist for offset")
|
||||
|
||||
# All is_opening JVs (current + next year) roll into FY 2024 opening
|
||||
self.assertEqual(
|
||||
year_2024_cash.opening,
|
||||
8000.0,
|
||||
"FY 2024 cash opening must combine is_opening JVs from current and next year",
|
||||
)
|
||||
self.assertEqual(
|
||||
year_2024_offset.opening,
|
||||
-8000.0,
|
||||
"FY 2024 offset opening must combine is_opening JVs from current and next year",
|
||||
)
|
||||
self.assertEqual(
|
||||
year_2024_cash.movement, 0.0, "Opening JVs must not be counted as period movement"
|
||||
)
|
||||
self.assertEqual(year_2024_cash.closing, 8000.0, "Closing = opening when no non-opening movement")
|
||||
|
||||
finally:
|
||||
frappe.db.set_single_value(
|
||||
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
|
||||
)
|
||||
|
||||
if pcv:
|
||||
pcv.reload()
|
||||
if pcv.docstatus == 1:
|
||||
pcv.cancel()
|
||||
|
||||
if jv_next_year and jv_next_year.docstatus == 1:
|
||||
jv_next_year.cancel()
|
||||
|
||||
if jv_current_year and jv_current_year.docstatus == 1:
|
||||
jv_current_year.cancel()
|
||||
|
||||
def test_account_with_gl_entries_but_no_prior_closing_balance(self):
|
||||
company = "_Test Company"
|
||||
cash_account = "_Test Cash - _TC"
|
||||
@@ -2025,3 +2179,210 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
|
||||
|
||||
finally:
|
||||
jv.cancel()
|
||||
|
||||
def test_pl_pcv_exclusion_and_growth_view_year_over_year(self):
|
||||
"""
|
||||
Sequence:
|
||||
1. Expense JV 2000 in FY 2024, PCV for FY 2024
|
||||
→ assert FY 2024 movement = 2000 via FinancialQueryBuilder
|
||||
2. Expense JV 3000 in FY 2025, PCV for FY 2025
|
||||
3. Run FinancialReportEngine with selected_view="Growth"
|
||||
→ assert col_2024 = 2000 (raw), col_2025 = 50.0 (% growth)
|
||||
"""
|
||||
company = "_Test Company"
|
||||
expense_account = "Administrative Expenses - _TC"
|
||||
bank_account = "_Test Bank - _TC"
|
||||
|
||||
template = None
|
||||
pcv_2024 = None
|
||||
pcv_2025 = None
|
||||
jv_2024 = None
|
||||
jv_2025 = None
|
||||
original_pcv_setting = frappe.db.get_single_value(
|
||||
"Accounts Settings", "use_legacy_controller_for_pcv"
|
||||
)
|
||||
|
||||
try:
|
||||
closing_account = frappe.db.get_value(
|
||||
"Account",
|
||||
{
|
||||
"company": company,
|
||||
"root_type": "Liability",
|
||||
"is_group": 0,
|
||||
"account_type": ["not in", ["Payable", "Receivable"]],
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||
|
||||
accounts = [
|
||||
frappe._dict(
|
||||
{
|
||||
"name": expense_account,
|
||||
"account_name": "Administrative Expenses",
|
||||
"account_number": "5001",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
# --- Step 1: FY 2024 expense + PCV, assert PCV reversal excluded ---
|
||||
jv_2024 = make_journal_entry(
|
||||
account1=expense_account,
|
||||
account2=bank_account,
|
||||
amount=2000,
|
||||
posting_date="2024-06-15",
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
fy_2024 = get_fiscal_year("2024-06-15", company=company)
|
||||
pcv_2024 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": "2024-12-31",
|
||||
"period_start_date": fy_2024[1],
|
||||
"period_end_date": fy_2024[2],
|
||||
"company": company,
|
||||
"fiscal_year": fy_2024[0],
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"closing_account_head": closing_account,
|
||||
"remarks": "Test PCV FY 2024",
|
||||
}
|
||||
)
|
||||
pcv_2024.insert()
|
||||
pcv_2024.submit()
|
||||
pcv_2024.reload()
|
||||
|
||||
builder_2024 = FinancialQueryBuilder(
|
||||
{
|
||||
"company": company,
|
||||
"from_fiscal_year": "2024",
|
||||
"to_fiscal_year": "2024",
|
||||
"period_start_date": "2024-01-01",
|
||||
"period_end_date": "2024-12-31",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Yearly",
|
||||
},
|
||||
[{"key": "2024", "from_date": "2024-01-01", "to_date": "2024-12-31"}],
|
||||
)
|
||||
data_2024 = builder_2024.fetch_account_balances(accounts)
|
||||
expense_2024 = data_2024.get(expense_account)
|
||||
self.assertIsNotNone(expense_2024, "Expense account must appear in FY 2024 results")
|
||||
year_2024 = expense_2024.get_period("2024")
|
||||
self.assertEqual(
|
||||
year_2024.movement,
|
||||
2000.0,
|
||||
"FY 2024 expense movement must equal real expense (PCV reversal excluded)",
|
||||
)
|
||||
|
||||
# --- Step 2: FY 2025 expense + PCV ---
|
||||
jv_2025 = make_journal_entry(
|
||||
account1=expense_account,
|
||||
account2=bank_account,
|
||||
amount=3000,
|
||||
posting_date="2025-06-15",
|
||||
company=company,
|
||||
submit=True,
|
||||
)
|
||||
fy_2025 = get_fiscal_year("2025-06-15", company=company)
|
||||
pcv_2025 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": "2025-12-31",
|
||||
"period_start_date": fy_2025[1],
|
||||
"period_end_date": fy_2025[2],
|
||||
"company": company,
|
||||
"fiscal_year": fy_2025[0],
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"closing_account_head": closing_account,
|
||||
"remarks": "Test PCV FY 2025",
|
||||
}
|
||||
)
|
||||
pcv_2025.insert()
|
||||
pcv_2025.submit()
|
||||
pcv_2025.reload()
|
||||
|
||||
# --- Step 3: full pipeline with Growth view across both years ---
|
||||
template_name = f"Test Growth Template {frappe.generate_hash()[:8]}"
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Financial Report Template",
|
||||
"template_name": template_name,
|
||||
"report_type": "Profit and Loss Statement",
|
||||
"rows": [
|
||||
{
|
||||
"reference_code": "EXP_ADMIN",
|
||||
"display_name": "Administrative Expenses",
|
||||
"indentation_level": 0,
|
||||
"data_source": "Account Data",
|
||||
"balance_type": "Closing Balance",
|
||||
"calculation_formula": f'["name", "=", "{expense_account}"]',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"report_template": template_name,
|
||||
"from_fiscal_year": fy_2024[0],
|
||||
"to_fiscal_year": fy_2025[0],
|
||||
"period_start_date": "2024-01-01",
|
||||
"period_end_date": "2025-12-31",
|
||||
"filter_based_on": "Date Range",
|
||||
"periodicity": "Yearly",
|
||||
"accumulated_values": 0,
|
||||
"selected_view": "Growth",
|
||||
}
|
||||
)
|
||||
|
||||
_columns, formatted_data, _msg, _chart = FinancialReportEngine().execute(filters)
|
||||
|
||||
expense_row = next(
|
||||
(row for row in formatted_data if row.get("account_name") == "Administrative Expenses"),
|
||||
None,
|
||||
)
|
||||
self.assertIsNotNone(expense_row, "Administrative Expenses row must appear in growth view")
|
||||
|
||||
period_keys = expense_row.get("_segment_info", {}).get("period_keys", [])
|
||||
self.assertEqual(len(period_keys), 2, "Yearly view must yield exactly two periods")
|
||||
first_period_key, second_period_key = period_keys
|
||||
|
||||
# First column: raw absolute value (FY 2024 expense)
|
||||
self.assertEqual(
|
||||
flt(expense_row[first_period_key]),
|
||||
2000.0,
|
||||
"First column in growth view must keep raw FY 2024 expense value",
|
||||
)
|
||||
# Second column: ((3000 - 2000) / 2000) * 100 = 50.0
|
||||
self.assertEqual(
|
||||
flt(expense_row[second_period_key]),
|
||||
50.0,
|
||||
"Second column must be % growth FY 2024 → FY 2025",
|
||||
)
|
||||
|
||||
finally:
|
||||
frappe.db.set_single_value(
|
||||
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
|
||||
)
|
||||
|
||||
if pcv_2025:
|
||||
pcv_2025.reload()
|
||||
if pcv_2025.docstatus == 1:
|
||||
pcv_2025.cancel()
|
||||
|
||||
if jv_2025 and jv_2025.docstatus == 1:
|
||||
jv_2025.cancel()
|
||||
|
||||
if pcv_2024:
|
||||
pcv_2024.reload()
|
||||
if pcv_2024.docstatus == 1:
|
||||
pcv_2024.cancel()
|
||||
|
||||
if jv_2024 and jv_2024.docstatus == 1:
|
||||
jv_2024.cancel()
|
||||
|
||||
if template and frappe.db.exists("Financial Report Template", template.name):
|
||||
frappe.delete_doc("Financial Report Template", template.name, force=1)
|
||||
|
||||
@@ -47,3 +47,12 @@ frappe.ui.form.on("Item Tax Template", {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Item Tax Template Detail", {
|
||||
not_applicable: function (frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.not_applicable) {
|
||||
frappe.model.set_value(cdt, cdn, "tax_rate", 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,8 +27,15 @@ class ItemTaxTemplate(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.set_zero_rate_for_not_applicable_tax()
|
||||
self.validate_tax_accounts()
|
||||
|
||||
def set_zero_rate_for_not_applicable_tax(self):
|
||||
"""Ensure tax_rate is 0 for any row marked as not applicable."""
|
||||
for row in self.get("taxes"):
|
||||
if row.not_applicable:
|
||||
row.tax_rate = 0
|
||||
|
||||
def autoname(self):
|
||||
if self.company and self.title:
|
||||
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"tax_type",
|
||||
"tax_rate"
|
||||
"tax_rate",
|
||||
"not_applicable"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -21,20 +22,30 @@
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate"
|
||||
"label": "Tax Rate",
|
||||
"read_only_depends_on": "eval:doc.not_applicable"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Check if this tax is not applicable to items (distinct from 0% rate)",
|
||||
"fieldname": "not_applicable",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Not Applicable"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:55.735360",
|
||||
"modified": "2025-12-26 17:19:18.791891",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class ItemTaxTemplateDetail(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
not_applicable: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -648,7 +648,7 @@ $.extend(erpnext.journal_entry, {
|
||||
reqd: 1,
|
||||
default: frm.doc.posting_date,
|
||||
},
|
||||
{ fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark") },
|
||||
{ fieldtype: "Small Text", fieldname: "remark", label: __("Remark") },
|
||||
{
|
||||
fieldtype: "Select",
|
||||
fieldname: "naming_series",
|
||||
@@ -665,8 +665,14 @@ $.extend(erpnext.journal_entry, {
|
||||
var values = dialog.get_values();
|
||||
|
||||
frm.set_value("posting_date", values.posting_date);
|
||||
frm.set_value("user_remark", values.user_remark);
|
||||
frm.set_value("naming_series", values.naming_series);
|
||||
if (values.remark) {
|
||||
frm.set_value("custom_remark", 1);
|
||||
frm.set_value("remark", values.remark);
|
||||
} else {
|
||||
frm.set_value("custom_remark", 0);
|
||||
frm.set_value("remark", "");
|
||||
}
|
||||
|
||||
// clear table is used because there might've been an error while adding child
|
||||
// and cleanup didn't happen
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"clearance_date",
|
||||
"column_break_oizh",
|
||||
"user_remark",
|
||||
"subscription_section",
|
||||
"auto_repeat_section",
|
||||
"auto_repeat",
|
||||
"tax_withholding_tab",
|
||||
"section_tax_withholding_entry",
|
||||
@@ -78,6 +78,7 @@
|
||||
"from_template",
|
||||
"title",
|
||||
"column_break3",
|
||||
"custom_remark",
|
||||
"remark",
|
||||
"mode_of_payment",
|
||||
"party_not_required"
|
||||
@@ -202,6 +203,7 @@
|
||||
{
|
||||
"fieldname": "user_remark",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "User Remark",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "user_remark",
|
||||
@@ -315,7 +317,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "remark",
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
"read_only_depends_on": "eval: !doc.custom_remark"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
|
||||
@@ -475,11 +477,6 @@
|
||||
"options": "Stock Entry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "auto_repeat",
|
||||
@@ -651,6 +648,17 @@
|
||||
"fieldname": "tax_withholding_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Tax Withholding"
|
||||
},
|
||||
{
|
||||
"fieldname": "auto_repeat_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "custom_remark",
|
||||
"fieldtype": "Check",
|
||||
"label": "Custom Remark"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -665,7 +673,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2026-03-09 17:15:26.569327",
|
||||
"modified": "2026-04-08 14:19:30.870894",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -61,6 +61,7 @@ class JournalEntry(AccountsController):
|
||||
cheque_no: DF.Data | None
|
||||
clearance_date: DF.Date | None
|
||||
company: DF.Link
|
||||
custom_remark: DF.Check
|
||||
difference: DF.Currency
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
@@ -1026,8 +1027,8 @@ class JournalEntry(AccountsController):
|
||||
if self.flags.skip_remarks_creation:
|
||||
return
|
||||
|
||||
if self.user_remark:
|
||||
r.append(_("Note: {0}").format(self.user_remark))
|
||||
if self.get("custom_remark"):
|
||||
return
|
||||
|
||||
if self.cheque_no:
|
||||
if self.cheque_date:
|
||||
@@ -1548,7 +1549,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
frappe.qb.from_(JournalEntry)
|
||||
.join(JournalEntryAccount)
|
||||
.on(JournalEntryAccount.parent == JournalEntry.name)
|
||||
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.user_remark)
|
||||
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.remark)
|
||||
.where(JournalEntryAccount.account == filters.get("account"))
|
||||
.where(JournalEntryAccount.reference_type.isnull() | (JournalEntryAccount.reference_type == ""))
|
||||
.where(JournalEntry.docstatus == 1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
frappe.listview_settings["Journal Entry"] = {
|
||||
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"],
|
||||
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.docstatus === 1) {
|
||||
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];
|
||||
|
||||
@@ -413,9 +413,9 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
|
||||
# Configure Repost Accounting Ledger for JVs
|
||||
settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
if not [x for x in settings.allowed_types if x.document_type == "Journal Entry"]:
|
||||
settings.append("allowed_types", {"document_type": "Journal Entry", "allowed": True})
|
||||
settings = frappe.get_doc("Accounts Settings")
|
||||
if "Journal Entry" not in [x.document_type for x in settings.repost_allowed_types]:
|
||||
settings.append("repost_allowed_types", {"document_type": "Journal Entry"})
|
||||
settings.save()
|
||||
|
||||
# Create JV with defaut cost center - _Test Cost Center
|
||||
@@ -523,7 +523,7 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = nowdate()
|
||||
jv.company = "_Test Company"
|
||||
jv.user_remark = "test"
|
||||
jv.remark = "test"
|
||||
jv.extend(
|
||||
"accounts",
|
||||
[
|
||||
@@ -592,6 +592,14 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
def test_custom_remark(self):
|
||||
# When custom_remark is enabled, remark should not be auto-overwritten on save
|
||||
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
|
||||
jv.custom_remark = 1
|
||||
jv.remark = "My custom remark text"
|
||||
jv.insert()
|
||||
self.assertEqual(jv.remark, "My custom remark text")
|
||||
|
||||
def test_credit_limit_for_customer(self):
|
||||
customer = make_customer("_Test New Customer")
|
||||
set_credit_limit("_Test New Customer", "_Test Company", 50)
|
||||
@@ -620,7 +628,7 @@ def make_journal_entry(
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = posting_date or nowdate()
|
||||
jv.company = company or "_Test Company"
|
||||
jv.user_remark = "test"
|
||||
jv.remark = "test"
|
||||
jv.multi_currency = 1
|
||||
jv.set(
|
||||
"accounts",
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-16 19:22:42.942264",
|
||||
"modified": "2026-04-14 18:16:47.795986",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Mode of Payment",
|
||||
@@ -68,12 +68,21 @@
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "HR User",
|
||||
"select": 1
|
||||
},
|
||||
{
|
||||
"role": "HR Manager",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"translated_doctype": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"remarks",
|
||||
"base_in_words",
|
||||
"is_opening",
|
||||
"title",
|
||||
"column_break_16",
|
||||
"letter_head",
|
||||
"print_heading",
|
||||
@@ -96,10 +97,9 @@
|
||||
"bank_account_no",
|
||||
"payment_order",
|
||||
"in_words",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"amended_from",
|
||||
"title"
|
||||
"auto_repeat_section",
|
||||
"auto_repeat"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -503,11 +503,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription Section"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "auto_repeat",
|
||||
@@ -781,6 +776,11 @@
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Tax Withholding Entries"
|
||||
},
|
||||
{
|
||||
"fieldname": "auto_repeat_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -2306,22 +2306,20 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
# Get positive outstanding sales /purchase invoices
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
condition = " and voucher_type={} and voucher_no={}".format(
|
||||
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
|
||||
)
|
||||
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
|
||||
common_filter.append(ple.voucher_type == args["voucher_type"])
|
||||
common_filter.append(ple.voucher_no == args["voucher_no"])
|
||||
|
||||
# Add cost center condition
|
||||
if args.get("cost_center"):
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
|
||||
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if args.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
|
||||
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
@@ -2330,18 +2328,19 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
}
|
||||
|
||||
for fieldname, date_fields in date_fields_dict.items():
|
||||
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
|
||||
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
|
||||
|
||||
if args.get(date_fields[0]) and args.get(date_fields[1]):
|
||||
condition += " and {} between '{}' and '{}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
condition += f" and {fieldname} between {from_date} and {to_date}"
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
elif args.get(date_fields[0]):
|
||||
# if only from date is supplied
|
||||
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
|
||||
condition += f" and {fieldname} >= {from_date}"
|
||||
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||
elif args.get(date_fields[1]):
|
||||
# if only to date is supplied
|
||||
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
|
||||
condition += f" and {fieldname} <= {to_date}"
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
@@ -2561,7 +2560,7 @@ def get_orders_to_be_billed(
|
||||
active_dimensions = get_dimensions(True)[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
|
||||
@@ -195,6 +195,30 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 100)
|
||||
|
||||
def test_reference_outstanding_amount_on_advance_pull(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
so = make_sales_order(qty=1, rate=1000)
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
pe.paid_amount = pe.received_amount = 500
|
||||
pe.references[0].allocated_amount = 500
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 500)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.allocate_advances_automatically = 1
|
||||
si.save()
|
||||
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
|
||||
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
|
||||
si.submit()
|
||||
|
||||
pe.load_from_db()
|
||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
|
||||
|
||||
def test_payment_entry_against_pi(self):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD",
|
||||
@@ -2105,6 +2129,37 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
self.assertEqual(ref.voucher_no, so.name)
|
||||
self.assertIsNotNone(ref.payment_term)
|
||||
|
||||
def test_project_name_in_exchange_gain_loss_entry(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_submit=True,
|
||||
)
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
|
||||
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
|
||||
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
|
||||
pe.source_exchange_rate = 100
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
rows = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
|
||||
fields=["project"],
|
||||
)
|
||||
self.assertEqual(len(rows), 2)
|
||||
|
||||
self.assertEqual(rows[0].project, si.project)
|
||||
self.assertEqual(rows[1].project, si.project)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
"depends_on": "eval:doc.is_a_subscription",
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription Section"
|
||||
"label": "Subscription"
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_plans",
|
||||
@@ -478,7 +478,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-13 12:53:00.963274",
|
||||
"modified": "2026-02-27 19:11:03.308896",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
from_date: frm.doc.period_start_date,
|
||||
to_date: frm.doc.period_end_date,
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
|
||||
@@ -18,9 +18,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||
|
||||
def test_closing_entry(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
@@ -70,9 +67,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_cost_center_wise_posting(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
|
||||
@@ -136,9 +130,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
def test_period_closing_with_finance_book_entries(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
surplus_account = create_account()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
@@ -190,9 +181,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertSequenceEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_gl_entries_restrictions(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
@@ -213,10 +201,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
|
||||
self.assertRaises(frappe.ValidationError, jv1.submit)
|
||||
|
||||
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
cost_center1 = create_cost_center("Test Cost Center 1")
|
||||
cost_center2 = create_cost_center("Test Cost Center 2")
|
||||
|
||||
@@ -201,7 +201,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
)
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
item_doc = make_item(
|
||||
"_Test Item With Batch FOR POS Merge Test",
|
||||
properties={
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
"due_date",
|
||||
"amended_from",
|
||||
"return_against",
|
||||
"section_break_abck",
|
||||
"title",
|
||||
"accounting_dimensions_section",
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
@@ -187,7 +189,7 @@
|
||||
"subscription_section",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"column_break_140",
|
||||
"auto_repeat_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"against_income_account"
|
||||
@@ -662,6 +664,7 @@
|
||||
"fieldname": "total_billing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Billing Amount",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1462,7 +1465,7 @@
|
||||
{
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription Section"
|
||||
"label": "Subscription"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -1480,10 +1483,6 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_140",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "auto_repeat",
|
||||
@@ -1533,6 +1532,7 @@
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -1619,12 +1619,29 @@
|
||||
{
|
||||
"fieldname": "column_break_bhao",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "auto_repeat_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_abck",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-10 14:23:07.181782",
|
||||
"modified": "2026-05-01 02:37:30.580568",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -172,6 +172,7 @@ class POSInvoice(SalesInvoice):
|
||||
terms: DF.TextEditor | None
|
||||
territory: DF.Link | None
|
||||
timesheets: DF.Table[SalesInvoiceTimesheet]
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_advance: DF.Currency
|
||||
|
||||
@@ -37,7 +37,6 @@ class POSInvoiceTestMixin(ERPNextTestSuite):
|
||||
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
||||
frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
|
||||
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")
|
||||
|
||||
@@ -34,7 +34,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
|
||||
@@ -64,7 +63,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||
@@ -123,7 +121,7 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
|
||||
item = "Test Selling Price Validation"
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
|
||||
|
||||
@@ -811,6 +811,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
@@ -857,7 +858,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-12 18:11:11.818015",
|
||||
"modified": "2026-04-20 16:16:12.322024",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
|
||||
@@ -17,7 +17,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestPricingRule(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
delete_existing_pricing_rules()
|
||||
setup_pricing_rule_data()
|
||||
self.enterClassContext(self.change_settings("Selling Settings", validate_selling_price=0))
|
||||
|
||||
@@ -1586,16 +1585,6 @@ def setup_pricing_rule_data():
|
||||
).insert()
|
||||
|
||||
|
||||
def delete_existing_pricing_rules():
|
||||
for doctype in [
|
||||
"Pricing Rule",
|
||||
"Pricing Rule Item Code",
|
||||
"Pricing Rule Item Group",
|
||||
"Pricing Rule Brand",
|
||||
]:
|
||||
frappe.db.sql(f"delete from `tab{doctype}`")
|
||||
|
||||
|
||||
def make_item_price(item, price_list_name, item_price):
|
||||
frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -662,7 +662,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if pricing_rule.is_recursive:
|
||||
transaction_qty = sum(
|
||||
[
|
||||
row.qty
|
||||
flt(row.qty)
|
||||
for row in doc.items
|
||||
if not row.is_free_item
|
||||
and row.item_code == args.item_code
|
||||
|
||||
@@ -115,7 +115,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
|
||||
if (
|
||||
doc.docstatus == 1 &&
|
||||
doc.outstanding_amount != 0 &&
|
||||
!doc.on_hold &&
|
||||
frappe.model.can_create("Payment Entry")
|
||||
) {
|
||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
@@ -130,7 +135,13 @@ 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.docstatus == 1 &&
|
||||
doc.outstanding_amount > 0 &&
|
||||
!cint(doc.is_return) &&
|
||||
!doc.on_hold &&
|
||||
frappe.boot.user.in_create.includes("Payment Request")
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
@@ -443,13 +454,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||
"expense_account",
|
||||
"discount_account",
|
||||
"cost_center",
|
||||
"project",
|
||||
]);
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = ["expense_account", "discount_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
|
||||
on_submit() {
|
||||
@@ -558,12 +570,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Purchase Invoice", {
|
||||
setup: function (frm) {
|
||||
frm.custom_make_buttons = {
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"update_billed_amount_in_purchase_receipt",
|
||||
"apply_tds",
|
||||
"amended_from",
|
||||
"section_break_hzux",
|
||||
"title",
|
||||
"supplier_invoice_details",
|
||||
"bill_no",
|
||||
"column_break_15",
|
||||
@@ -180,11 +182,12 @@
|
||||
"unrealized_profit_loss_account",
|
||||
"subscription_section",
|
||||
"subscription",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"column_break_114",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"automation_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"printing_settings",
|
||||
"letter_head",
|
||||
"group_same_items",
|
||||
@@ -1675,6 +1678,24 @@
|
||||
"fieldname": "totals_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "automation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Automation"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_hzux",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1682,7 +1703,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-25 11:45:38.696888",
|
||||
"modified": "2026-04-28 07:15:31.062404",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -203,6 +203,7 @@ class PurchaseInvoice(BuyingController):
|
||||
taxes_and_charges_deducted: DF.Currency
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_advance: DF.Currency
|
||||
|
||||
@@ -2256,9 +2256,9 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
def test_repost_accounting_entries(self):
|
||||
# update repost settings
|
||||
settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]:
|
||||
settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True})
|
||||
settings = frappe.get_doc("Accounts Settings")
|
||||
if "Purchase Invoice" not in [x.document_type for x in settings.repost_allowed_types]:
|
||||
settings.append("repost_allowed_types", {"document_type": "Purchase Invoice"})
|
||||
settings.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
|
||||
@@ -219,7 +219,6 @@ def get_allowed_types_from_settings(child_doc: bool = False):
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types",
|
||||
filters={"allowed": True},
|
||||
fields=["document_type"],
|
||||
distinct=True,
|
||||
)
|
||||
@@ -274,14 +273,13 @@ def validate_docs_for_voucher_types(doc_voucher_types):
|
||||
if disallowed_types := voucher_types.difference(allowed_types):
|
||||
message = "are" if len(disallowed_types) > 1 else "is"
|
||||
frappe.throw(
|
||||
_("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format(
|
||||
_(
|
||||
"{0} {1} not allowed to be reposted. You can enable it by adding it '{2}' table in {3}."
|
||||
).format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
message,
|
||||
frappe.bold(
|
||||
frappe.utils.get_link_to_form(
|
||||
"Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings"
|
||||
)
|
||||
),
|
||||
frappe.bold("Allowed Doctype"),
|
||||
frappe.utils.get_link_to_form("Accounts Settings"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -289,8 +287,6 @@ def validate_docs_for_voucher_types(doc_voucher_types):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
|
||||
filters = {"allowed": True}
|
||||
|
||||
if txt:
|
||||
filters.update({"document_type": ("like", f"%{txt}%")})
|
||||
|
||||
|
||||
@@ -203,6 +203,11 @@ class TestRepostAccountingLedger(ERPNextTestSuite):
|
||||
def test_06_repost_purchase_receipt(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
if not frappe.db.set_value("Company", "_Test Company", "service_expense_account"):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "service_expense_account", "Marketing Expenses - _TC"
|
||||
)
|
||||
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
@@ -275,7 +280,8 @@ def update_repost_settings():
|
||||
"Journal Entry",
|
||||
"Purchase Receipt",
|
||||
]
|
||||
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
|
||||
for x in allowed_types:
|
||||
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
|
||||
repost_settings.save()
|
||||
settings = frappe.get_doc("Accounts Settings")
|
||||
for _type in allowed_types:
|
||||
if _type not in [x.document_type for x in settings.repost_allowed_types]:
|
||||
settings.append("repost_allowed_types", {"document_type": _type})
|
||||
settings.save()
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Repost Accounting Ledger Settings", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2023-11-07 09:57:20.619939",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"allowed_types"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "allowed_types",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allowed Doctypes",
|
||||
"options": "Repost Allowed Types"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 0,
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-16 13:28:21.312607",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
|
||||
|
||||
|
||||
class RepostAccountingLedgerSettings(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
|
||||
|
||||
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
|
||||
|
||||
allowed_types: DF.Table[RepostAllowedTypes]
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.update_property_for_accounting_dimension()
|
||||
|
||||
def update_property_for_accounting_dimension(self):
|
||||
doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed]
|
||||
if not doctypes:
|
||||
return
|
||||
doctypes += get_child_docs(doctypes)
|
||||
|
||||
set_allow_on_submit_for_dimension_fields(doctypes)
|
||||
|
||||
|
||||
def set_allow_on_submit_for_dimension_fields(doctypes):
|
||||
for dt in doctypes:
|
||||
meta = frappe.get_meta(dt)
|
||||
for dimension in get_accounting_dimensions():
|
||||
df = meta.get_field(dimension)
|
||||
if df and not df.allow_on_submit:
|
||||
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)
|
||||
@@ -1,11 +0,0 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestRepostAccountingLedgerSettings(ERPNextTestSuite):
|
||||
pass
|
||||
@@ -6,9 +6,7 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"column_break_sfzb",
|
||||
"allowed"
|
||||
"document_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -17,29 +15,20 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Doctype",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allowed",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Allowed"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sfzb",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:32.415806",
|
||||
"modified": "2026-04-14 16:53:16.806714",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Allowed Types",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ class RepostAllowedTypes(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
allowed: DF.Check
|
||||
document_type: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
|
||||
@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
|
||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
@@ -135,13 +135,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
if (doc.outstanding_amount > 0) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request_with_schedule();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
if (frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request_with_schedule();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
this.frm.add_custom_button(
|
||||
__("Invoice Discounting"),
|
||||
this.make_invoice_discounting.bind(this),
|
||||
@@ -552,12 +554,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||
"income_account",
|
||||
"discount_account",
|
||||
"cost_center",
|
||||
]);
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = ["income_account", "discount_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
|
||||
set_dynamic_labels() {
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
"is_created_using_pos",
|
||||
"pos_closing_entry",
|
||||
"has_subcontracted",
|
||||
"section_break_qllv",
|
||||
"title",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -91,9 +93,9 @@
|
||||
"column_break_xjag",
|
||||
"base_rounding_adjustment",
|
||||
"base_rounded_total",
|
||||
"section_break_vacb",
|
||||
"section_break_pxwz",
|
||||
"total_advance",
|
||||
"column_break_rdks",
|
||||
"column_break_iaso",
|
||||
"outstanding_amount",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
@@ -199,6 +201,7 @@
|
||||
"unrealized_profit_loss_account",
|
||||
"against_income_account",
|
||||
"commission_section",
|
||||
"column_break_rdiw",
|
||||
"sales_partner",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break10",
|
||||
@@ -214,12 +217,14 @@
|
||||
"language",
|
||||
"subscription_section",
|
||||
"subscription",
|
||||
"from_date",
|
||||
"auto_repeat",
|
||||
"column_break_140",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"automation_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"utm_analytics_section",
|
||||
"column_break_rdke",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_ixxw",
|
||||
@@ -1147,6 +1152,7 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Rounding Adjustment",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1159,6 +1165,7 @@
|
||||
"label": "Rounded Total",
|
||||
"oldfieldname": "rounded_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -2304,14 +2311,6 @@
|
||||
"options": "fa fa-group",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_vacb",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rdks",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ixxw",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -2321,6 +2320,40 @@
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "UTM Analytics"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "automation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Automation"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pxwz",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rdke",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rdiw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iaso",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_qllv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -2334,7 +2367,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2026-03-09 17:15:30.931929",
|
||||
"modified": "2026-05-01 02:37:29.742764",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -225,6 +225,7 @@ class SalesInvoice(SellingController):
|
||||
terms: DF.TextEditor | None
|
||||
territory: DF.Link | None
|
||||
timesheets: DF.Table[SalesInvoiceTimesheet]
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_advance: DF.Currency
|
||||
|
||||
@@ -2025,10 +2025,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
def test_multiple_uom_in_selling(self):
|
||||
frappe.db.sql(
|
||||
"""delete from `tabItem Price`
|
||||
where price_list='_Test Price List' and item_code='_Test Item'"""
|
||||
)
|
||||
item_price = frappe.new_doc("Item Price")
|
||||
item_price.price_list = "_Test Price List"
|
||||
item_price.item_code = "_Test Item"
|
||||
|
||||
@@ -10,8 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestShareTransfer(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabShare Transfer`")
|
||||
frappe.db.sql("delete from `tabShare Balance`")
|
||||
share_transfers = [
|
||||
{
|
||||
"doctype": "Share Transfer",
|
||||
|
||||
@@ -25,6 +25,10 @@ frappe.ui.form.on("Shipping Rule", {
|
||||
},
|
||||
calculate_based_on: function (frm) {
|
||||
frm.trigger("toggle_reqd");
|
||||
if (frm.doc.calculate_based_on === "Fixed") {
|
||||
frm.clear_table("conditions");
|
||||
frm.refresh_field("conditions");
|
||||
}
|
||||
},
|
||||
toggle_reqd: function (frm) {
|
||||
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed");
|
||||
|
||||
@@ -58,6 +58,11 @@ class ShippingRule(Document):
|
||||
self.validate_overlapping_shipping_rule_conditions()
|
||||
|
||||
def validate_from_to_values(self):
|
||||
if self.calculate_based_on == "Fixed":
|
||||
if self.conditions:
|
||||
self.set("conditions", [])
|
||||
return
|
||||
|
||||
zero_to_values = []
|
||||
|
||||
for d in self.get("conditions"):
|
||||
|
||||
@@ -5,8 +5,7 @@ import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.utils import add_days, add_months, today
|
||||
from frappe.utils import add_days, add_months, getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
@@ -1922,7 +1921,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
|
||||
def set_previous_fy_and_tax_category(self):
|
||||
test_company = "_Test Company"
|
||||
category = "Cumulative Threshold TDS"
|
||||
|
||||
def add_company_to_fy(fy, company):
|
||||
if not [x.company for x in fy.companies if x.company == company]:
|
||||
@@ -1948,20 +1946,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
)
|
||||
self.prev_fy.save()
|
||||
|
||||
# setup tax withholding category for previous fiscal year
|
||||
cat = frappe.get_doc("Tax Withholding Category", category)
|
||||
cat.append(
|
||||
"rates",
|
||||
{
|
||||
"from_date": self.prev_fy.year_start_date,
|
||||
"to_date": self.prev_fy.year_end_date,
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 0,
|
||||
"cumulative_threshold": 30000,
|
||||
},
|
||||
)
|
||||
cat.save()
|
||||
|
||||
def test_tds_across_fiscal_year(self):
|
||||
"""
|
||||
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
|
||||
@@ -1972,6 +1956,14 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
supplier = "Test TDS Supplier"
|
||||
# Cumulative threshold 30000 and tax rate 10%
|
||||
category = "Cumulative Threshold TDS"
|
||||
create_tax_withholding_category(
|
||||
category_name=category,
|
||||
rate=10,
|
||||
from_date=self.prev_fy.year_start_date,
|
||||
to_date=self.prev_fy.year_end_date,
|
||||
account="TDS - _TC",
|
||||
cumulative_threshold=30000,
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Supplier",
|
||||
supplier,
|
||||
@@ -2043,6 +2035,158 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
self.assertEqual(pi2.taxes, [])
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
|
||||
def test_threshold_resets_in_new_fiscal_year(self):
|
||||
"""
|
||||
Threshold entries from a previous FY must not carry over into the new FY.
|
||||
"""
|
||||
self.set_previous_fy_and_tax_category()
|
||||
invoices = []
|
||||
supplier = "Test TDS Supplier"
|
||||
category = "Cumulative Threshold TDS"
|
||||
create_tax_withholding_category(
|
||||
category_name=category,
|
||||
rate=10,
|
||||
from_date=self.prev_fy.year_start_date,
|
||||
to_date=self.prev_fy.year_end_date,
|
||||
account="TDS - _TC",
|
||||
cumulative_threshold=30000,
|
||||
)
|
||||
self.setup_party_with_category("Supplier", supplier, category)
|
||||
prev_fy_date = add_days(self.prev_fy.year_end_date, -10)
|
||||
|
||||
# Previous FY: 3 invoices to cross the 30000 cumulative threshold
|
||||
for _ in range(3):
|
||||
pi = create_purchase_invoice(supplier=supplier, posting_date=prev_fy_date, set_posting_time=True)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Third invoice crosses the threshold - 3000 TDS deducted across all three
|
||||
self.validate_tax_deduction(invoices[-1], 3000)
|
||||
|
||||
# Current FY: 10000 invoice - must be Under Withheld, threshold resets
|
||||
pi_curr = create_purchase_invoice(supplier=supplier)
|
||||
pi_curr.submit()
|
||||
invoices.append(pi_curr)
|
||||
self.validate_tax_deduction(pi_curr, 0)
|
||||
|
||||
self.validate_tax_withholding_entries(
|
||||
"Purchase Invoice",
|
||||
pi_curr.name,
|
||||
[
|
||||
self.get_tax_withholding_entry(
|
||||
tax_withholding_category=category,
|
||||
party_type="Supplier",
|
||||
party=supplier,
|
||||
taxable_doctype="Purchase Invoice",
|
||||
taxable_name=pi_curr.name,
|
||||
tax_rate=10.0,
|
||||
taxable_amount=10000.0,
|
||||
withholding_amount=0.0,
|
||||
status="Under Withheld",
|
||||
withholding_doctype=None,
|
||||
withholding_name=None,
|
||||
under_withheld_reason=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
self.cleanup_invoices(invoices)
|
||||
|
||||
def test_tax_on_excess_threshold_resets_in_new_fiscal_year(self):
|
||||
"""
|
||||
For tax-on-excess categories, unused threshold must reset each FY.
|
||||
"""
|
||||
self.set_previous_fy_and_tax_category()
|
||||
invoices = []
|
||||
supplier = "Test TDS Supplier3"
|
||||
category = "New TDS Category"
|
||||
create_tax_withholding_category(
|
||||
category_name=category,
|
||||
rate=10,
|
||||
from_date=self.prev_fy.year_start_date,
|
||||
to_date=self.prev_fy.year_end_date,
|
||||
account="TDS - _TC",
|
||||
cumulative_threshold=30000,
|
||||
tax_on_excess_amount=1,
|
||||
round_off_tax_amount=1,
|
||||
)
|
||||
self.setup_party_with_category("Supplier", supplier, category)
|
||||
prev_fy_date = add_days(self.prev_fy.year_end_date, -10)
|
||||
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier=supplier, posting_date=prev_fy_date, set_posting_time=True)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
pi3 = create_purchase_invoice(
|
||||
supplier=supplier, rate=20000, posting_date=prev_fy_date, set_posting_time=True
|
||||
)
|
||||
pi3.submit()
|
||||
invoices.append(pi3)
|
||||
|
||||
self.validate_tax_deduction(pi3, 1000)
|
||||
self.validate_tax_withholding_entries(
|
||||
"Purchase Invoice",
|
||||
pi3.name,
|
||||
[
|
||||
self.get_tax_withholding_entry(
|
||||
tax_withholding_category=category,
|
||||
party_type="Supplier",
|
||||
party=supplier,
|
||||
taxable_doctype="Purchase Invoice",
|
||||
taxable_name=pi3.name,
|
||||
tax_rate=10.0,
|
||||
taxable_amount=10000.0,
|
||||
withholding_amount=0.0,
|
||||
status="Settled",
|
||||
withholding_doctype="Purchase Invoice",
|
||||
withholding_name=pi3.name,
|
||||
under_withheld_reason="Threshold Exemption",
|
||||
),
|
||||
self.get_tax_withholding_entry(
|
||||
tax_withholding_category=category,
|
||||
party_type="Supplier",
|
||||
party=supplier,
|
||||
taxable_doctype="Purchase Invoice",
|
||||
taxable_name=pi3.name,
|
||||
tax_rate=10.0,
|
||||
taxable_amount=10000.0,
|
||||
withholding_amount=1000.0,
|
||||
status="Settled",
|
||||
withholding_doctype="Purchase Invoice",
|
||||
withholding_name=pi3.name,
|
||||
under_withheld_reason=None,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
# no excess, so no TDS
|
||||
pi_curr = create_purchase_invoice(supplier=supplier, rate=30000)
|
||||
pi_curr.submit()
|
||||
invoices.append(pi_curr)
|
||||
self.validate_tax_deduction(pi_curr, 0)
|
||||
|
||||
self.validate_tax_withholding_entries(
|
||||
"Purchase Invoice",
|
||||
pi_curr.name,
|
||||
[
|
||||
self.get_tax_withholding_entry(
|
||||
tax_withholding_category=category,
|
||||
party_type="Supplier",
|
||||
party=supplier,
|
||||
taxable_doctype="Purchase Invoice",
|
||||
taxable_name=pi_curr.name,
|
||||
tax_rate=10.0,
|
||||
taxable_amount=30000.0,
|
||||
withholding_amount=0.0,
|
||||
status="Settled",
|
||||
withholding_doctype="Purchase Invoice",
|
||||
withholding_name=pi_curr.name,
|
||||
under_withheld_reason="Threshold Exemption",
|
||||
),
|
||||
],
|
||||
)
|
||||
self.cleanup_invoices(invoices)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||
def test_tds_payment_entry_cancellation(self):
|
||||
"""
|
||||
@@ -3997,7 +4141,7 @@ def create_tax_withholding_category(
|
||||
tax_deduction_basis="Net Total",
|
||||
):
|
||||
if not frappe.db.exists("Tax Withholding Category", category_name):
|
||||
frappe.get_doc(
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": category_name,
|
||||
@@ -4018,6 +4162,22 @@ def create_tax_withholding_category(
|
||||
"accounts": [{"company": "_Test Company", "account": account}],
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
doc = frappe.get_doc("Tax Withholding Category", category_name)
|
||||
if not any(getdate(r.from_date) == getdate(from_date) for r in doc.rates):
|
||||
doc.append(
|
||||
"rates",
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"tax_withholding_rate": rate,
|
||||
"single_threshold": single_threshold,
|
||||
"cumulative_threshold": cumulative_threshold,
|
||||
},
|
||||
)
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def create_lower_deduction_certificate(
|
||||
|
||||
@@ -640,6 +640,7 @@ class TaxWithholdingController:
|
||||
.where(entry.tax_withholding_category == category.name)
|
||||
.where(entry.company == self.doc.company)
|
||||
.where(entry.docstatus == 1)
|
||||
.where(entry.taxable_date.between(category.from_date, category.to_date))
|
||||
.groupby(entry.status)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,118 +1,147 @@
|
||||
[
|
||||
{
|
||||
"account_category_name": "Cash and Cash Equivalents",
|
||||
"root_type": "Asset",
|
||||
"description": "Cash on hand, demand deposits, and short-term highly liquid investments readily convertible to cash with original maturities of three months or less. Examples: Cash in hand, bank current accounts, money market funds, treasury bills \u22643 months."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Cost of Goods Sold",
|
||||
"root_type": "Expense",
|
||||
"description": "Direct costs attributable to cost of goods sold. Examples: Raw materials, stock in trade."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Current Tax Liabilities",
|
||||
"root_type": "Liability",
|
||||
"description": "Income tax obligations for current and prior periods. Examples: Provision for income tax, advance tax paid, tax deducted at source."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Finance Costs",
|
||||
"root_type": "Expense",
|
||||
"description": "Interest and financing-related expenses. Examples: Interest on borrowings, bank charges, lease interest, foreign exchange losses."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Intangible Assets",
|
||||
"root_type": "Asset",
|
||||
"description": "Identifiable non-monetary assets without physical substance. Examples: Software, patents, trademarks, licenses, development costs."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Investment Income",
|
||||
"root_type": "Income",
|
||||
"description": "Returns generated from financial investments and cash management. Examples: Interest income, dividend income, rental income, fair value gains."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Long-term Borrowings",
|
||||
"root_type": "Liability",
|
||||
"description": "Interest-bearing debt obligations with maturity beyond one year. Examples: Term loans, bonds, debentures, mortgages."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Long-term Investments",
|
||||
"root_type": "Asset",
|
||||
"description": "Investments held for strategic purposes or extended periods. Examples: Equity investments, bonds, associates, joint ventures, deposits."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Long-term Provisions",
|
||||
"root_type": "Liability",
|
||||
"description": "Present obligations beyond one year with uncertain timing/amount. Examples: Asset retirement obligations, environmental remediation, legal settlements."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Operating Expenses",
|
||||
"root_type": "Expense",
|
||||
"description": "Costs incurred in ordinary business operations excluding direct costs. Examples: Selling expenses, administrative costs, marketing, utilities, rent."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Current Assets",
|
||||
"root_type": "Asset",
|
||||
"description": "Current assets not classified elsewhere including prepaid expenses and advances. Examples: Prepaid insurance, prepaid rent, advance to suppliers, security deposits recoverable within one year."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Current Liabilities",
|
||||
"root_type": "Liability",
|
||||
"description": "Short-term obligations not classified elsewhere. Examples: Accrued expenses, statutory liabilities, employee payables."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Direct Costs",
|
||||
"root_type": "Expense",
|
||||
"description": "Direct costs excluding cost of goods sold. Examples: Direct labor, manufacturing overhead, freight inward."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Non-current Assets",
|
||||
"root_type": "Asset",
|
||||
"description": "Long-term assets not classified elsewhere. Examples: Security deposits, long-term prepayments, advances for capital goods."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Non-current Liabilities",
|
||||
"root_type": "Liability",
|
||||
"description": "Long-term obligations not classified elsewhere. Examples: Long-term deposits, deferred income, government grants."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Operating Income",
|
||||
"root_type": "Income",
|
||||
"description": "Incidental income related to business operations but not core revenue. Examples: Scrap sales, government grants, insurance claims, foreign exchange gains."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Payables",
|
||||
"root_type": "Liability",
|
||||
"description": "Non-trade payables and obligations to parties other than suppliers. Examples: Employee payables, accrued expenses, customer advances, security deposits received."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Other Receivables",
|
||||
"root_type": "Asset",
|
||||
"description": "Non-trade amounts due to the entity excluding financing arrangements. Examples: Employee advances, insurance claims, tax refunds, deposits recoverable."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Reserves and Surplus",
|
||||
"root_type": "Equity",
|
||||
"description": "Accumulated profits and other reserves created from profits or share premium. Examples: General reserves, retained earnings, statutory reserves, share premium."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Revenue from Operations",
|
||||
"root_type": "Income",
|
||||
"description": "Income from primary business activities in ordinary course. Examples: Sales of goods, service revenue, commission income, royalty income."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Share Capital",
|
||||
"root_type": "Equity",
|
||||
"description": "Nominal value of issued and paid-up equity shares. Examples: Common stock, ordinary shares, preference shares."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Short-term Borrowings",
|
||||
"root_type": "Liability",
|
||||
"description": "Interest-bearing debt obligations due within one year. Examples: Bank overdrafts, short-term loans, current portion of long-term debt."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Short-term Investments",
|
||||
"root_type": "Asset",
|
||||
"description": "Financial instruments held for short-term investment purposes, readily convertible to cash. Examples: Marketable securities, fixed deposits >3 months, mutual funds."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Short-term Provisions",
|
||||
"root_type": "Liability",
|
||||
"description": "Present obligations due within one year with uncertain timing or amount. Examples: Warranty provisions, legal claims, restructuring costs."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Stock Assets",
|
||||
"root_type": "Asset",
|
||||
"description": "Inventory and stock-related assets including raw materials, work in progress, finished goods, and stock in trade. Examples: Raw materials, finished goods, trading merchandise, consumables."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Tangible Assets",
|
||||
"root_type": "Asset",
|
||||
"description": "Physical assets used in business operations including property, plant, and equipment. Examples: Land, buildings, machinery, equipment, vehicles, furniture, capital work in progress."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Tax Expense",
|
||||
"root_type": "Expense",
|
||||
"description": "Current and deferred income tax obligations. Examples: Current tax provision, deferred tax expense, withholding taxes."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Trade Payables",
|
||||
"root_type": "Liability",
|
||||
"description": "Amounts owed to suppliers. Examples: Supplier invoices, accrued purchases, bills payable."
|
||||
},
|
||||
{
|
||||
"account_category_name": "Trade Receivables",
|
||||
"root_type": "Asset",
|
||||
"description": "Amounts due from customers for goods sold or services provided in ordinary course of business. Examples: Accounts receivable, notes receivable from customers, unbilled revenue."
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -36,7 +36,8 @@ def make_gl_entries(
|
||||
):
|
||||
if gl_map:
|
||||
if (
|
||||
not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
|
||||
not cancel
|
||||
and not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
|
||||
and gl_map[0].voucher_type != "Period Closing Voucher"
|
||||
):
|
||||
bud_val = BudgetValidation(gl_map=gl_map)
|
||||
|
||||
@@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestAccountBalance(ERPNextTestSuite):
|
||||
def test_account_balance(self):
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
||||
|
||||
filters = {
|
||||
"company": "_Test Company 2",
|
||||
"report_date": getdate(),
|
||||
|
||||
@@ -34,6 +34,17 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_account",
|
||||
label: __("Payable Account"),
|
||||
|
||||
@@ -117,3 +117,49 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
def test_project_filter(self):
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
|
||||
).insert()
|
||||
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.project = project.name
|
||||
pi.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"project": [project.name],
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
row = report[0]
|
||||
self.assertEqual(row.project, project.name)
|
||||
self.assertEqual(row.invoiced, 300.0)
|
||||
|
||||
def test_project_on_report_output(self):
|
||||
"""
|
||||
Report row must carry the invoice's project.
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
|
||||
).insert()
|
||||
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.project = project.name
|
||||
pi.save().submit()
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])
|
||||
|
||||
@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
|
||||
@@ -36,6 +36,17 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
|
||||
@@ -196,6 +196,7 @@ class ReceivablePayableReport:
|
||||
and ple.against_voucher_type in self.advance_payment_doctypes
|
||||
):
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
self.voucher_balance[key].project = ple.project
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -362,6 +363,7 @@ class ReceivablePayableReport:
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
project,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
@@ -390,6 +392,7 @@ class ReceivablePayableReport:
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
"project",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
@@ -931,6 +934,7 @@ class ReceivablePayableReport:
|
||||
ple.against_voucher_no,
|
||||
ple.party_type,
|
||||
ple.cost_center,
|
||||
ple.project,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
@@ -998,6 +1002,9 @@ class ReceivablePayableReport:
|
||||
if self.filters.cost_center:
|
||||
self.get_cost_center_conditions()
|
||||
|
||||
if self.filters.project:
|
||||
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
|
||||
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def get_cost_center_conditions(self):
|
||||
@@ -1237,6 +1244,7 @@ class ReceivablePayableReport:
|
||||
)
|
||||
|
||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
|
||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||
self.add_column(
|
||||
label=_("Voucher No"),
|
||||
@@ -1413,6 +1421,7 @@ class InitSQLProceduresForAR:
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
project {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
@@ -1432,6 +1441,7 @@ class InitSQLProceduresForAR:
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
project {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
@@ -1447,7 +1457,7 @@ class InitSQLProceduresForAR:
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
@@ -1489,7 +1499,7 @@ class InitSQLProceduresForAR:
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
|
||||
@@ -774,22 +774,18 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
|
||||
def test_party_account_filter(self):
|
||||
si1 = self.create_sales_invoice()
|
||||
self.customer2 = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "Jane Doe",
|
||||
"type": "Individual",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
jane = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "Jane Doe",
|
||||
"type": "Individual",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
).insert()
|
||||
|
||||
self.customer = jane.name
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.posting_date = add_days(today(), -1)
|
||||
si2.customer = self.customer2.name
|
||||
si2.currency = "USD"
|
||||
si2.conversion_rate = 80
|
||||
si2.debit_to = self.debtors_usd
|
||||
@@ -997,22 +993,18 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertEqual(expected_data, report_output)
|
||||
|
||||
def test_future_payments_on_foreign_currency(self):
|
||||
self.customer2 = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "Jane Doe",
|
||||
"type": "Individual",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
)
|
||||
.insert()
|
||||
.submit()
|
||||
)
|
||||
jane = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "Jane Doe",
|
||||
"type": "Individual",
|
||||
"default_currency": "USD",
|
||||
}
|
||||
).insert()
|
||||
self.customer = jane.name
|
||||
|
||||
si = self.create_sales_invoice(do_not_submit=True)
|
||||
si.posting_date = add_days(today(), -1)
|
||||
si.customer = self.customer2.name
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.debit_to = self.debtors_usd
|
||||
@@ -1204,3 +1196,52 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
def test_project_filter(self):
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
|
||||
).insert()
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.project = project.name
|
||||
si.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"project": [project.name],
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
row = report[0]
|
||||
self.assertEqual(row.project, project.name)
|
||||
self.assertEqual(row.invoiced, 100.0)
|
||||
|
||||
def test_project_on_report_output(self):
|
||||
"""
|
||||
Report row must carry the invoice's project even when the payment entry
|
||||
has no project set.
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
|
||||
).insert()
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.project = project.name
|
||||
si.save().submit()
|
||||
|
||||
# payment has no project — report row must still show the invoice's project
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])
|
||||
|
||||
@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FinancialReportEngine,
|
||||
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
|
||||
@@ -13,9 +13,6 @@ COMPANY_SHORT_NAME = "_TC6"
|
||||
|
||||
class TestBalanceSheet(ERPNextTestSuite):
|
||||
def test_balance_sheet(self):
|
||||
frappe.db.sql(f"delete from `tabJournal Entry` where company='{COMPANY}'")
|
||||
frappe.db.sql(f"delete from `tabGL Entry` where company='{COMPANY}'")
|
||||
|
||||
create_account("VAT Liabilities", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
|
||||
create_account("Advance VAT Paid", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
|
||||
create_account("My Bank", f"Bank Accounts - {COMPANY_SHORT_NAME}", COMPANY)
|
||||
|
||||
@@ -12,6 +12,7 @@ from pypika import Order
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FinancialReportEngine,
|
||||
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_columns,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FinancialReportEngine,
|
||||
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -774,7 +774,28 @@ class GrossProfitGenerator:
|
||||
# IMP NOTE
|
||||
# stock_ledger_entries should already be filtered by item_code and warehouse and
|
||||
# sorted by posting_date desc, posting_time desc
|
||||
if item_code in self.non_stock_items and (row.project or row.cost_center):
|
||||
if (
|
||||
row.delivered_by_supplier
|
||||
and row.so_detail
|
||||
and (
|
||||
po_details := frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters={"sales_order_item": row.so_detail, "docstatus": 1},
|
||||
pluck="name",
|
||||
)
|
||||
)
|
||||
):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty * table.base_net_rate))
|
||||
.where((table.po_detail.isin(po_details)) & (table.docstatus == 1))
|
||||
)
|
||||
return flt(query.run()[0][0])
|
||||
|
||||
elif item_code in self.non_stock_items and (row.project or row.cost_center):
|
||||
# Issue 6089-Get last purchasing rate for non-stock item
|
||||
item_rate = self.get_last_purchase_rate(item_code, row)
|
||||
return flt(row.qty) * item_rate
|
||||
@@ -804,26 +825,6 @@ class GrossProfitGenerator:
|
||||
return self.calculate_buying_amount_from_sle(
|
||||
row, my_sle, parenttype, parent, item_row, item_code
|
||||
)
|
||||
elif (
|
||||
row.delivered_by_supplier
|
||||
and row.so_detail
|
||||
and (
|
||||
po_details := frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters={"sales_order_item": row.so_detail, "docstatus": 1},
|
||||
pluck="name",
|
||||
)
|
||||
)
|
||||
):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.stock_qty * table.base_net_rate))
|
||||
.where((table.po_detail.isin(po_details)) & (table.docstatus == 1))
|
||||
)
|
||||
return flt(query.run()[0][0])
|
||||
elif row.sales_order and row.so_detail:
|
||||
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
|
||||
if incoming_amount:
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FinancialReportEngine,
|
||||
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
|
||||
@@ -499,7 +499,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
|
||||
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
|
||||
from `tabPurchase Taxes and Charges`
|
||||
where parent in (%s) and category in ('Total', 'Valuation and Total')
|
||||
and base_tax_amount_after_discount_amount != 0
|
||||
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
|
||||
group by parent, account_head, add_deduct_tax
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
|
||||
@@ -5,14 +5,12 @@ import frappe
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext.accounts.report.purchase_register.purchase_register import execute
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPurchaseRegister(ERPNextTestSuite):
|
||||
def test_purchase_register(self):
|
||||
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
|
||||
|
||||
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
|
||||
|
||||
pi = make_purchase_invoice()
|
||||
@@ -26,10 +24,50 @@ class TestPurchaseRegister(ERPNextTestSuite):
|
||||
self.assertEqual(first_row.total_tax, 100)
|
||||
self.assertEqual(first_row.grand_total, 1100)
|
||||
|
||||
def test_purchase_register_ledger_view(self):
|
||||
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
|
||||
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
|
||||
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
|
||||
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company 6",
|
||||
supplier="_Test Supplier",
|
||||
item="_Test Item",
|
||||
warehouse="_Test Warehouse - _TC6",
|
||||
cost_center="_Test Cost Center - _TC6",
|
||||
do_not_save=1,
|
||||
do_not_submit=1,
|
||||
qty=1,
|
||||
rate=1000,
|
||||
)
|
||||
pr.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "GST - _TC6",
|
||||
"cost_center": "_Test Cost Center - _TC6",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Valuation and Total",
|
||||
"charge_type": "Actual",
|
||||
"description": "PR Tax",
|
||||
"tax_amount": 100.0,
|
||||
"rate": 100,
|
||||
},
|
||||
)
|
||||
pr.insert()
|
||||
pr.submit()
|
||||
|
||||
# Mimic custom naming collision across doctypes (same parent value in shared child table).
|
||||
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
|
||||
|
||||
report_results = execute(filters)
|
||||
first_row = frappe._dict(report_results[1][0])
|
||||
|
||||
self.assertEqual(first_row.voucher_no, pi.name)
|
||||
self.assertEqual(first_row.total_tax, 100)
|
||||
self.assertEqual(first_row.grand_total, 1100)
|
||||
|
||||
def test_purchase_register_ledger_view(self):
|
||||
filters = frappe._dict(
|
||||
company="_Test Company 6",
|
||||
from_date=add_months(today(), -1),
|
||||
|
||||
@@ -4,6 +4,7 @@ from frappe.utils import getdate, today
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.sales_register.sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -72,6 +73,43 @@ class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
|
||||
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
|
||||
si = self.create_sales_invoice(rate=98)
|
||||
|
||||
# Real workflow setup: create a Sales Order with taxes in the shared child table.
|
||||
so = make_sales_order(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
rate=77,
|
||||
do_not_save=1,
|
||||
do_not_submit=1,
|
||||
)
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": self.income_account,
|
||||
"description": "SO Tax",
|
||||
"tax_amount": 55.0,
|
||||
},
|
||||
)
|
||||
so.insert()
|
||||
so.submit()
|
||||
|
||||
# Mimic custom naming collision across doctypes (same parent value in shared child table).
|
||||
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
result = frappe._dict(report[1][0])
|
||||
self.assertEqual(result.voucher_no, si.name)
|
||||
self.assertEqual(result.net_total, 98.0)
|
||||
self.assertEqual(result.tax_total, 0)
|
||||
self.assertEqual(result.grand_total, 98.0)
|
||||
|
||||
def test_journal_with_cost_center_filter(self):
|
||||
je1 = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -6,299 +6,282 @@ from frappe import _
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
"""Generate Tax Withholding Details report"""
|
||||
validate_filters(filters)
|
||||
class TaxWithholdingDetailsReport:
|
||||
party_types = ("Customer", "Supplier")
|
||||
document_types = ("Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry")
|
||||
|
||||
# Process and format data
|
||||
data = get_tax_withholding_data(filters)
|
||||
columns = get_columns(filters)
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
self.entries = []
|
||||
self.doc_info = {}
|
||||
self.party_details = {}
|
||||
|
||||
return columns, data
|
||||
@classmethod
|
||||
def execute(cls, filters=None):
|
||||
return cls(filters).run()
|
||||
|
||||
def run(self):
|
||||
self.validate_filters()
|
||||
return self.get_columns(), self.get_data()
|
||||
|
||||
def validate_filters(filters):
|
||||
"""Validate report filters"""
|
||||
filters = frappe._dict(filters or {})
|
||||
def validate_filters(self):
|
||||
if not self.filters.from_date or not self.filters.to_date:
|
||||
frappe.throw(_("From Date and To Date are required"))
|
||||
|
||||
if not filters.from_date or not filters.to_date:
|
||||
frappe.throw(_("From Date and To Date are required"))
|
||||
if self.filters.from_date > self.filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
def get_data(self):
|
||||
self.entries = self.get_entries_query().run(as_dict=True)
|
||||
if not self.entries:
|
||||
return []
|
||||
|
||||
self.doc_info = self.fetch_additional_doc_info()
|
||||
self.party_details = self.fetch_party_details()
|
||||
return self.build_rows()
|
||||
|
||||
def get_tax_withholding_data(filters):
|
||||
"""Process entries into final report format"""
|
||||
data = []
|
||||
entries = get_tax_withholding_entries(filters)
|
||||
if not entries:
|
||||
return data
|
||||
def build_rows(self):
|
||||
rows = []
|
||||
for entry in self.entries:
|
||||
doc_details = (
|
||||
self.doc_info.get((entry.transaction_type, entry.ref_no), {}) if entry.ref_no else {}
|
||||
)
|
||||
party_info = self.party_details.get((entry.party_type, entry.party), {})
|
||||
rows.append({**entry, **doc_details, **party_info})
|
||||
|
||||
doc_info = get_additional_doc_info(entries)
|
||||
party_details = get_party_details(entries)
|
||||
rows.sort(
|
||||
key=lambda x: (
|
||||
x["tax_withholding_category"] or "",
|
||||
x["transaction_date"] or "",
|
||||
x["withholding_name"] or "",
|
||||
)
|
||||
)
|
||||
return rows
|
||||
|
||||
for entry in entries:
|
||||
doc_details = frappe._dict()
|
||||
if entry.taxable_name:
|
||||
doc_details = doc_info.get((entry.taxable_doctype, entry.taxable_name), {})
|
||||
def get_entries_query(self):
|
||||
twe = frappe.qb.DocType("Tax Withholding Entry")
|
||||
query = (
|
||||
frappe.qb.from_(twe)
|
||||
.select(
|
||||
twe.party_type,
|
||||
twe.party,
|
||||
IfNull(twe.tax_id, "").as_("tax_id"),
|
||||
twe.tax_withholding_category,
|
||||
twe.taxable_amount.as_("total_amount"),
|
||||
twe.tax_rate.as_("rate"),
|
||||
twe.withholding_amount.as_("tax_amount"),
|
||||
IfNull(twe.taxable_doctype, "").as_("transaction_type"),
|
||||
IfNull(twe.taxable_name, "").as_("ref_no"),
|
||||
twe.taxable_date,
|
||||
IfNull(twe.withholding_doctype, "").as_("withholding_doctype"),
|
||||
IfNull(twe.withholding_name, "").as_("withholding_name"),
|
||||
twe.withholding_date.as_("transaction_date"),
|
||||
)
|
||||
.where(twe.docstatus == 1)
|
||||
.where(twe.withholding_date >= self.filters.from_date)
|
||||
.where(twe.withholding_date <= self.filters.to_date)
|
||||
.where(IfNull(twe.withholding_name, "") != "")
|
||||
.where(twe.status != "Duplicate")
|
||||
)
|
||||
|
||||
party_info = party_details.get((entry.party_type, entry.party), {})
|
||||
if self.filters.company:
|
||||
query = query.where(twe.company == self.filters.company)
|
||||
if self.filters.party_type:
|
||||
query = query.where(twe.party_type == self.filters.party_type)
|
||||
if self.filters.party:
|
||||
query = query.where(twe.party == self.filters.party)
|
||||
|
||||
row = {
|
||||
"section_code": entry.tax_withholding_category,
|
||||
"entity_type": party_info.get("entity_type"),
|
||||
"rate": entry.tax_rate,
|
||||
"total_amount": entry.taxable_amount,
|
||||
"grand_total": doc_details.get("grand_total", 0),
|
||||
"base_total": doc_details.get("base_total", 0),
|
||||
"tax_amount": entry.withholding_amount,
|
||||
"transaction_date": entry.withholding_date,
|
||||
"transaction_type": entry.taxable_doctype,
|
||||
"ref_no": entry.taxable_name,
|
||||
"taxable_date": entry.taxable_date,
|
||||
"supplier_invoice_no": doc_details.get("bill_no"),
|
||||
"supplier_invoice_date": doc_details.get("bill_date"),
|
||||
"withholding_doctype": entry.withholding_doctype,
|
||||
"withholding_name": entry.withholding_name,
|
||||
"party_name": party_info.get("party_name"),
|
||||
"tax_id": entry.tax_id,
|
||||
"party": entry.party,
|
||||
"party_type": entry.party_type,
|
||||
}
|
||||
data.append(row)
|
||||
return query
|
||||
|
||||
# Sort by section code and transaction date
|
||||
data.sort(key=lambda x: (x["section_code"] or "", x["transaction_date"] or ""))
|
||||
return data
|
||||
def fetch_party_details(self):
|
||||
parties_by_type = {pt: set() for pt in self.party_types}
|
||||
for entry in self.entries:
|
||||
if entry.party_type in parties_by_type and entry.party:
|
||||
parties_by_type[entry.party_type].add(entry.party)
|
||||
|
||||
party_map = {}
|
||||
for party_type, party_set in parties_by_type.items():
|
||||
if not party_set:
|
||||
continue
|
||||
|
||||
def get_party_details(entries):
|
||||
"""Fetch party details in batch for all entries"""
|
||||
party_map = frappe._dict()
|
||||
parties_by_type = {"Customer": set(), "Supplier": set()}
|
||||
query = self.get_party_query(party_type, party_set)
|
||||
if query is None:
|
||||
continue
|
||||
|
||||
# Group parties by type
|
||||
for entry in entries:
|
||||
if entry.party_type in parties_by_type and entry.party:
|
||||
parties_by_type[entry.party_type].add(entry.party)
|
||||
for row in query.run(as_dict=True):
|
||||
party_map[(party_type, row.pop("name"))] = row
|
||||
|
||||
# Batch fetch for each party type
|
||||
for party_type, party_set in parties_by_type.items():
|
||||
if not party_type or not party_set:
|
||||
continue
|
||||
return party_map
|
||||
|
||||
def get_party_query(self, party_type, party_set):
|
||||
doctype = frappe.qb.DocType(party_type)
|
||||
fields = [doctype.name]
|
||||
|
||||
if party_type == "Supplier":
|
||||
fields.extend([doctype.supplier_type.as_("entity_type"), doctype.supplier_name.as_("party_name")])
|
||||
fields.extend(
|
||||
[
|
||||
doctype.supplier_type.as_("party_entity_type"),
|
||||
doctype.supplier_name.as_("party_name"),
|
||||
]
|
||||
)
|
||||
elif party_type == "Customer":
|
||||
fields.extend([doctype.customer_type.as_("entity_type"), doctype.customer_name.as_("party_name")])
|
||||
fields.extend(
|
||||
[
|
||||
doctype.customer_type.as_("party_entity_type"),
|
||||
doctype.customer_name.as_("party_name"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(party_set))
|
||||
party_details = query.run(as_dict=True)
|
||||
return frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(party_set))
|
||||
|
||||
for party in party_details:
|
||||
party_map[(party_type, party.name)] = party
|
||||
def fetch_additional_doc_info(self):
|
||||
docs_by_type = {dt: set() for dt in self.document_types}
|
||||
for entry in self.entries:
|
||||
if entry.ref_no and entry.transaction_type in docs_by_type:
|
||||
docs_by_type[entry.transaction_type].add(entry.ref_no)
|
||||
|
||||
return party_map
|
||||
doc_info = {}
|
||||
for doctype_name, voucher_set in docs_by_type.items():
|
||||
if not voucher_set:
|
||||
continue
|
||||
|
||||
query = self.get_doc_info_query(doctype_name, voucher_set)
|
||||
if query is None:
|
||||
continue
|
||||
|
||||
for row in query.run(as_dict=True):
|
||||
doc_info[(doctype_name, row.pop("name"))] = row
|
||||
|
||||
return doc_info
|
||||
|
||||
def get_doc_info_query(self, doctype_name, voucher_set):
|
||||
if doctype_name == "Purchase Invoice":
|
||||
get_doc_fields = self.get_purchase_invoice_fields
|
||||
elif doctype_name == "Sales Invoice":
|
||||
get_doc_fields = self.get_sales_invoice_fields
|
||||
elif doctype_name == "Payment Entry":
|
||||
get_doc_fields = self.get_payment_entry_fields
|
||||
elif doctype_name == "Journal Entry":
|
||||
get_doc_fields = self.get_journal_entry_fields
|
||||
else:
|
||||
return None
|
||||
|
||||
doctype = frappe.qb.DocType(doctype_name)
|
||||
fields = [doctype.name, *get_doc_fields(doctype)]
|
||||
return frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(voucher_set))
|
||||
|
||||
def get_purchase_invoice_fields(self, doctype):
|
||||
return [
|
||||
doctype.grand_total,
|
||||
doctype.base_total,
|
||||
doctype.bill_no.as_("supplier_invoice_no"),
|
||||
doctype.bill_date.as_("supplier_invoice_date"),
|
||||
]
|
||||
|
||||
def get_sales_invoice_fields(self, doctype):
|
||||
return [doctype.grand_total, doctype.base_total]
|
||||
|
||||
def get_payment_entry_fields(self, doctype):
|
||||
return [
|
||||
doctype.paid_amount_after_tax.as_("grand_total"),
|
||||
doctype.base_paid_amount.as_("base_total"),
|
||||
]
|
||||
|
||||
def get_journal_entry_fields(self, doctype):
|
||||
return [doctype.total_debit.as_("grand_total"), doctype.total_debit.as_("base_total")]
|
||||
|
||||
def get_columns(self):
|
||||
party_type = self.filters.get("party_type", "Party")
|
||||
return [
|
||||
{
|
||||
"label": _("Tax Withholding Category"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"width": 90,
|
||||
},
|
||||
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
|
||||
{
|
||||
"label": _(f"{party_type} Name"),
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _(party_type),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _(f"{party_type} Type"),
|
||||
"fieldname": "party_entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Invoice No"),
|
||||
"fieldname": "supplier_invoice_no",
|
||||
"fieldtype": "Data",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Invoice Date"),
|
||||
"fieldname": "supplier_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Tax Rate %"), "fieldname": "rate", "fieldtype": "Percent", "width": 60},
|
||||
{
|
||||
"label": _("Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Tax Amount"), "fieldname": "tax_amount", "fieldtype": "Currency", "width": 120},
|
||||
{
|
||||
"label": _("Grand Total (Company Currency)"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total (Transaction Currency)"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 170,
|
||||
},
|
||||
{"label": _("Reference Date"), "fieldname": "taxable_date", "fieldtype": "Date", "width": 100},
|
||||
{
|
||||
"label": _("Transaction Type"),
|
||||
"fieldname": "transaction_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Reference No."),
|
||||
"fieldname": "ref_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "transaction_type",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Date of Transaction"),
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Withholding Document"),
|
||||
"fieldname": "withholding_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "withholding_doctype",
|
||||
"width": 150,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
"""Generate report columns based on filters"""
|
||||
columns = [
|
||||
{
|
||||
"label": _("Section Code"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "section_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 90,
|
||||
},
|
||||
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
|
||||
{
|
||||
"label": _(f"{filters.get('party_type', 'Party')} Name"),
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _(filters.get("party_type", "Party")),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Entity Type"),
|
||||
"fieldname": "entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Invoice No"),
|
||||
"fieldname": "supplier_invoice_no",
|
||||
"fieldtype": "Data",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier Invoice Date"),
|
||||
"fieldname": "supplier_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Rate %"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"width": 60,
|
||||
},
|
||||
{
|
||||
"label": _("Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total (Company Currency)"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total (Transaction Currency)"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 170,
|
||||
},
|
||||
{
|
||||
"label": _("Reference Date"),
|
||||
"fieldname": "taxable_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Transaction Type"),
|
||||
"fieldname": "transaction_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Reference No."),
|
||||
"fieldname": "ref_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "transaction_type",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Date of Transaction"),
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Withholding Document"),
|
||||
"fieldname": "withholding_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "withholding_doctype",
|
||||
"width": 150,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_tax_withholding_entries(filters):
|
||||
twe = frappe.qb.DocType("Tax Withholding Entry")
|
||||
query = (
|
||||
frappe.qb.from_(twe)
|
||||
.select(
|
||||
twe.company,
|
||||
twe.party_type,
|
||||
twe.party,
|
||||
IfNull(twe.tax_id, "").as_("tax_id"),
|
||||
twe.tax_withholding_category,
|
||||
IfNull(twe.tax_withholding_group, "").as_("tax_withholding_group"),
|
||||
twe.taxable_amount,
|
||||
twe.tax_rate,
|
||||
twe.withholding_amount,
|
||||
IfNull(twe.taxable_doctype, "").as_("taxable_doctype"),
|
||||
IfNull(twe.taxable_name, "").as_("taxable_name"),
|
||||
twe.taxable_date,
|
||||
IfNull(twe.under_withheld_reason, "").as_("under_withheld_reason"),
|
||||
IfNull(twe.lower_deduction_certificate, "").as_("lower_deduction_certificate"),
|
||||
IfNull(twe.withholding_doctype, "").as_("withholding_doctype"),
|
||||
IfNull(twe.withholding_name, "").as_("withholding_name"),
|
||||
twe.withholding_date,
|
||||
twe.status,
|
||||
)
|
||||
.where(twe.docstatus == 1)
|
||||
.where(twe.withholding_date >= filters.from_date)
|
||||
.where(twe.withholding_date <= filters.to_date)
|
||||
.where(IfNull(twe.withholding_name, "") != "")
|
||||
.where(twe.status != "Duplicate")
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(twe.company == filters.get("company"))
|
||||
|
||||
if filters.get("party_type"):
|
||||
query = query.where(twe.party_type == filters.get("party_type"))
|
||||
|
||||
if filters.get("party"):
|
||||
query = query.where(twe.party == filters.get("party"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_additional_doc_info(entries):
|
||||
"""Fetch additional document information in batch"""
|
||||
doc_info = {}
|
||||
docs_by_type = {
|
||||
"Purchase Invoice": set(),
|
||||
"Sales Invoice": set(),
|
||||
"Payment Entry": set(),
|
||||
"Journal Entry": set(),
|
||||
}
|
||||
|
||||
# Group documents by type
|
||||
for entry in entries:
|
||||
if entry.taxable_name and entry.taxable_doctype in docs_by_type:
|
||||
docs_by_type[entry.taxable_doctype].add(entry.taxable_name)
|
||||
|
||||
for doctype_name, voucher_set in docs_by_type.items():
|
||||
if voucher_set:
|
||||
_fetch_doc_info(doctype_name, voucher_set, doc_info)
|
||||
|
||||
return doc_info
|
||||
|
||||
|
||||
def _fetch_doc_info(doctype_name, voucher_set, doc_info):
|
||||
doctype = frappe.qb.DocType(doctype_name)
|
||||
fields = [doctype.name]
|
||||
|
||||
# Add doctype-specific fields
|
||||
if doctype_name == "Purchase Invoice":
|
||||
fields.extend([doctype.grand_total, doctype.base_total, doctype.bill_no, doctype.bill_date])
|
||||
elif doctype_name == "Sales Invoice":
|
||||
fields.extend([doctype.grand_total, doctype.base_total])
|
||||
elif doctype_name == "Payment Entry":
|
||||
fields.extend(
|
||||
[doctype.paid_amount_after_tax.as_("grand_total"), doctype.base_paid_amount.as_("base_total")]
|
||||
)
|
||||
elif doctype_name == "Journal Entry":
|
||||
fields.extend([doctype.total_debit.as_("grand_total"), doctype.total_debit.as_("base_total")])
|
||||
else:
|
||||
return
|
||||
|
||||
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(voucher_set))
|
||||
entries = query.run(as_dict=True)
|
||||
|
||||
for entry in entries:
|
||||
doc_info[(doctype_name, entry.name)] = entry
|
||||
execute = TaxWithholdingDetailsReport.execute
|
||||
|
||||
@@ -5,10 +5,12 @@ import frappe
|
||||
from frappe.utils import add_to_date, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
|
||||
create_purchase_invoice,
|
||||
create_records,
|
||||
create_sales_invoice,
|
||||
create_tax_withholding_category,
|
||||
make_journal_entry_with_tax_withholding,
|
||||
)
|
||||
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
@@ -20,43 +22,45 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.clear_old_entries()
|
||||
create_tax_accounts()
|
||||
create_records()
|
||||
|
||||
def test_tax_withholding_for_customers(self):
|
||||
create_tax_category(cumulative_threshold=300)
|
||||
frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
|
||||
si = create_sales_invoice(rate=1000)
|
||||
pe = create_tcs_payment_entry()
|
||||
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "TCS")
|
||||
si = create_sales_invoice(customer="Test TCS Customer", rate=1000)
|
||||
si.submit()
|
||||
|
||||
create_tcs_payment_entry()
|
||||
jv = create_tcs_journal_entry()
|
||||
|
||||
filters = frappe._dict(
|
||||
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
|
||||
)
|
||||
result = execute(filters)[1]
|
||||
|
||||
expected_values = [
|
||||
# Check for JV totals using back calculation logic
|
||||
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
|
||||
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
|
||||
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
|
||||
[jv.name, "TCS", 0.075, 1000.75, 0.75, 1000.75],
|
||||
["", "TCS", 0.075, None, 0.75, None],
|
||||
[si.name, "TCS", 0.075, 1000.0, 0.75, 1000.75],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def test_single_account_for_multiple_categories(self):
|
||||
create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1.tax_withholding_category = "TDS - 1"
|
||||
create_tax_category("TDS - 1", rate=10, account="TDS - _TC", cumulative_threshold=1)
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 1")
|
||||
inv_1 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
|
||||
inv_1.submit()
|
||||
|
||||
create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
|
||||
inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_2.tax_withholding_category = "TDS - 2"
|
||||
create_tax_category("TDS - 2", rate=20, account="TDS - _TC", cumulative_threshold=1)
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 2")
|
||||
inv_2 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
|
||||
inv_2.submit()
|
||||
result = execute(
|
||||
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
|
||||
)[1]
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
|
||||
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
|
||||
[inv_1.name, "TDS - 1", 10, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 2", 20, 5000, 1000, 4000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
@@ -81,20 +85,21 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", tds_doc.name)
|
||||
inv_1 = create_purchase_invoice(
|
||||
supplier="Test TDS Supplier",
|
||||
rate=5000,
|
||||
posting_date=add_to_date(fiscal_year[1], days=1),
|
||||
set_posting_time=True,
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2 = create_purchase_invoice(
|
||||
supplier="Test TDS Supplier",
|
||||
rate=5000,
|
||||
posting_date=from_date,
|
||||
set_posting_time=True,
|
||||
)
|
||||
inv_2.submit()
|
||||
|
||||
result = execute(
|
||||
@@ -113,12 +118,13 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def check_expected_values(self, result, expected_values):
|
||||
self.assertEqual(len(result), len(expected_values))
|
||||
for i in range(len(result)):
|
||||
voucher = frappe._dict(result[i])
|
||||
voucher_expected_values = expected_values[i]
|
||||
voucher_actual_values = (
|
||||
voucher.ref_no,
|
||||
voucher.section_code,
|
||||
voucher.tax_withholding_category,
|
||||
voucher.rate,
|
||||
voucher.base_total,
|
||||
voucher.tax_amount,
|
||||
@@ -127,21 +133,6 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
|
||||
|
||||
|
||||
def create_tax_accounts():
|
||||
account_names = ["TCS", "TDS"]
|
||||
for account in account_names:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"company": "_Test Company",
|
||||
"account_name": account,
|
||||
"parent_account": "Duties and Taxes - _TC",
|
||||
"report_type": "Balance Sheet",
|
||||
"root_type": "Liability",
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
|
||||
|
||||
def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
from_date = fiscal_year[1]
|
||||
@@ -157,55 +148,34 @@ def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulat
|
||||
)
|
||||
|
||||
|
||||
def create_tcs_payment_entry():
|
||||
def create_tcs_payment_entry(party="Test TCS Customer", category="TCS", amount=1000):
|
||||
"""Create a TCS Payment Entry that generates a Tax Withholding Entry (Over Withheld)."""
|
||||
payment_entry = create_payment_entry(
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party="_Test Customer",
|
||||
party=party,
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="Cash - _TC",
|
||||
paid_amount=2550,
|
||||
)
|
||||
|
||||
payment_entry.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "TCS - _TC",
|
||||
"charge_type": "Actual",
|
||||
"tax_amount": 0.53,
|
||||
"add_deduct_tax": "Add",
|
||||
"description": "Test",
|
||||
"cost_center": "Main - _TC",
|
||||
},
|
||||
paid_amount=amount,
|
||||
)
|
||||
payment_entry.apply_tds = 1
|
||||
payment_entry.tax_withholding_category = category
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
return payment_entry
|
||||
|
||||
|
||||
def create_tcs_journal_entry():
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = today()
|
||||
jv.company = "_Test Company"
|
||||
jv.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 10000,
|
||||
},
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"debit_in_account_currency": 9992.5,
|
||||
},
|
||||
{
|
||||
"account": "TCS - _TC",
|
||||
"debit_in_account_currency": 7.5,
|
||||
},
|
||||
],
|
||||
def create_tcs_journal_entry(party="Test TCS Customer", category="TCS", amount=1000):
|
||||
"""Create a TCS Credit Note Journal Entry that generates a Tax Withholding Entry."""
|
||||
jv = make_journal_entry_with_tax_withholding(
|
||||
party_type="Customer",
|
||||
party=party,
|
||||
voucher_type="Credit Note",
|
||||
amount=amount,
|
||||
save=False,
|
||||
)
|
||||
jv.insert()
|
||||
return jv.submit()
|
||||
jv.apply_tds = 1
|
||||
jv.tax_withholding_category = category
|
||||
jv.save()
|
||||
jv.submit()
|
||||
return jv
|
||||
|
||||
@@ -2,121 +2,94 @@ import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
|
||||
get_tax_withholding_data,
|
||||
TaxWithholdingDetailsReport,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
class TDSComputationSummaryReport(TaxWithholdingDetailsReport):
|
||||
GROUP_BY_FIELDS = ("party_type", "party", "tax_withholding_category")
|
||||
CARRY_OVER_FIELDS = (
|
||||
"tax_id",
|
||||
"party",
|
||||
"party_type",
|
||||
"party_name",
|
||||
"tax_withholding_category",
|
||||
"party_entity_type",
|
||||
"rate",
|
||||
)
|
||||
AGGREGATE_FIELDS = ("total_amount", "tax_amount")
|
||||
|
||||
data = get_tax_withholding_data(filters)
|
||||
columns = get_columns(filters)
|
||||
def validate_filters(self):
|
||||
if self.filters.from_date > self.filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
final_result = group_by_party_and_category(data, filters)
|
||||
from_year = get_fiscal_year(self.filters.from_date)[0]
|
||||
to_year = get_fiscal_year(self.filters.to_date)[0]
|
||||
if from_year != to_year:
|
||||
frappe.throw(_("From Date and To Date lie in different Fiscal Year"))
|
||||
|
||||
return columns, final_result
|
||||
self.filters.fiscal_year = from_year
|
||||
|
||||
def get_data(self):
|
||||
return self.group_rows(super().get_data())
|
||||
|
||||
def validate_filters(filters):
|
||||
"""Validate if dates are properly set and lie in the same fiscal year"""
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
def group_rows(self, data):
|
||||
grouped = {}
|
||||
for row in data:
|
||||
key = tuple(row.get(f) for f in self.GROUP_BY_FIELDS)
|
||||
bucket = grouped.setdefault(
|
||||
key,
|
||||
{
|
||||
**{f: row.get(f) for f in self.CARRY_OVER_FIELDS},
|
||||
**{f: 0.0 for f in self.AGGREGATE_FIELDS},
|
||||
},
|
||||
)
|
||||
|
||||
from_year = get_fiscal_year(filters.from_date)[0]
|
||||
to_year = get_fiscal_year(filters.to_date)[0]
|
||||
if from_year != to_year:
|
||||
frappe.throw(_("From Date and To Date lie in different Fiscal Year"))
|
||||
for f in self.AGGREGATE_FIELDS:
|
||||
bucket[f] += row.get(f) or 0.0
|
||||
|
||||
filters["fiscal_year"] = from_year
|
||||
return list(grouped.values())
|
||||
|
||||
|
||||
def group_by_party_and_category(data, filters):
|
||||
party_category_wise_map = {}
|
||||
|
||||
for row in data:
|
||||
party_category_wise_map.setdefault(
|
||||
(row.get("party"), row.get("section_code")),
|
||||
def get_columns(self):
|
||||
party_type = self.filters.get("party_type", "Party")
|
||||
return [
|
||||
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
|
||||
{
|
||||
"tax_id": row.get("tax_id"),
|
||||
"party": row.get("party"),
|
||||
"party_name": row.get("party_name"),
|
||||
"section_code": row.get("section_code"),
|
||||
"entity_type": row.get("entity_type"),
|
||||
"rate": row.get("rate"),
|
||||
"total_amount": 0.0,
|
||||
"tax_amount": 0.0,
|
||||
"label": _(party_type),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"width": 180,
|
||||
},
|
||||
)
|
||||
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get(
|
||||
"total_amount", 0.0
|
||||
)
|
||||
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get(
|
||||
"tax_amount", 0.0
|
||||
)
|
||||
|
||||
final_result = get_final_result(party_category_wise_map)
|
||||
|
||||
return final_result
|
||||
{
|
||||
"label": _(f"{party_type} Name"),
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Withholding Category"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _(f"{party_type} Type"),
|
||||
"fieldname": "party_entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{"label": _("Tax Rate %"), "fieldname": "rate", "fieldtype": "Percent", "width": 120},
|
||||
{
|
||||
"label": _("Total Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Tax Amount"), "fieldname": "tax_amount", "fieldtype": "Float", "width": 120},
|
||||
]
|
||||
|
||||
|
||||
def get_final_result(party_category_wise_map):
|
||||
out = []
|
||||
for _key, value in party_category_wise_map.items():
|
||||
out.append(value)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
|
||||
{
|
||||
"label": _(filters.get("party_type")),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _(f"{filters.get('party_type', 'Party')} Name"),
|
||||
"fieldname": "party_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Section Code"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "section_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Entity Type"),
|
||||
"fieldname": "entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Rate %"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Total Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
execute = TDSComputationSummaryReport.execute
|
||||
|
||||
@@ -42,9 +42,6 @@ class TestTrialBalance(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
|
||||
|
||||
branch1 = frappe.new_doc("Branch")
|
||||
branch1.branch = "Location 1"
|
||||
branch1.insert(ignore_if_duplicate=True)
|
||||
|
||||
@@ -546,7 +546,7 @@ def reconcile_against_document(
|
||||
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
|
||||
dimensions_dict=dimensions_dict,
|
||||
)
|
||||
if referenced_row.get("outstanding_amount"):
|
||||
if referenced_row.get("outstanding_amount") and entry.get("outstanding_amount") is None:
|
||||
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
|
||||
|
||||
reposting_rows.append(referenced_row)
|
||||
@@ -2513,6 +2513,7 @@ def create_gain_loss_journal(
|
||||
ref2_detail_no,
|
||||
cost_center,
|
||||
dimensions,
|
||||
project=None,
|
||||
) -> str:
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
@@ -2539,6 +2540,7 @@ def create_gain_loss_journal(
|
||||
"account_currency": party_account_currency,
|
||||
"exchange_rate": 0,
|
||||
"cost_center": cost_center or erpnext.get_default_cost_center(company),
|
||||
"project": project,
|
||||
"reference_type": ref1_dt,
|
||||
"reference_name": ref1_dn,
|
||||
"reference_detail_no": ref1_detail_no,
|
||||
@@ -2556,6 +2558,7 @@ def create_gain_loss_journal(
|
||||
"account_currency": gain_loss_account_currency,
|
||||
"exchange_rate": 1,
|
||||
"cost_center": cost_center or erpnext.get_default_cost_center(company),
|
||||
"project": project,
|
||||
"reference_type": ref2_dt,
|
||||
"reference_name": ref2_dn,
|
||||
"reference_detail_no": ref2_detail_no,
|
||||
|
||||
@@ -26,7 +26,6 @@ class TestAssetCapitalization(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
set_depreciation_settings_in_company()
|
||||
create_asset_capitalization_data()
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_capitalization_with_perpetual_inventory(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
@@ -195,6 +195,9 @@ def reschedule_depreciation(asset_doc, notes, disposal_date=None):
|
||||
for row in asset_doc.get("finance_books"):
|
||||
current_schedule = get_asset_depr_schedule_doc(asset_doc.name, None, row.finance_book)
|
||||
|
||||
if disposal_date and flt(row.value_after_depreciation) <= flt(row.expected_value_after_useful_life):
|
||||
continue
|
||||
|
||||
if current_schedule:
|
||||
if current_schedule.docstatus == 1:
|
||||
new_schedule = frappe.copy_doc(current_schedule)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"allow_zero_qty_in_supplier_quotation",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"allow_zero_qty_in_request_for_quotation",
|
||||
"allow_negative_rates_for_items",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"allow_multiple_items",
|
||||
@@ -279,6 +280,12 @@
|
||||
"fieldname": "validate_consumed_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Consumed Qty (as per BOM)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_negative_rates_for_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Negative rates for Items"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -288,7 +295,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-16 13:28:19.432589",
|
||||
"modified": "2026-04-15 16:07:35.484787",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -18,6 +18,7 @@ class BuyingSettings(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
allow_multiple_items: DF.Check
|
||||
allow_negative_rates_for_items: DF.Check
|
||||
allow_zero_qty_in_purchase_order: DF.Check
|
||||
allow_zero_qty_in_request_for_quotation: DF.Check
|
||||
allow_zero_qty_in_supplier_quotation: DF.Check
|
||||
|
||||
@@ -416,7 +416,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||
if (
|
||||
frappe.model.can_create("Payment Entry") &&
|
||||
flt(doc.per_billed) < 100 &&
|
||||
doc.status != "Delivered"
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
() => this.make_payment_entry(),
|
||||
@@ -424,7 +428,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
|
||||
if (flt(doc.per_billed) < 100) {
|
||||
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
@@ -660,12 +664,20 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (doc.schedule_date) {
|
||||
row.schedule_date = doc.schedule_date;
|
||||
refresh_field("schedule_date", cdn, "items");
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
|
||||
field_copy.push("project");
|
||||
}
|
||||
if (doc.schedule_date) {
|
||||
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
|
||||
} else {
|
||||
field_copy.push("schedule_date");
|
||||
}
|
||||
if (field_copy.length) {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,12 +752,6 @@ cur_frm.cscript.update_status = function (label, status) {
|
||||
});
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
if (cur_frm.doc.is_old_subcontracting_flow) {
|
||||
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"is_subcontracted",
|
||||
"has_unit_price_items",
|
||||
"supplier_warehouse",
|
||||
"section_break_ahub",
|
||||
"title",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -147,7 +149,7 @@
|
||||
"column_break_86",
|
||||
"select_print_heading",
|
||||
"language",
|
||||
"subscription_section",
|
||||
"auto_repeat_section",
|
||||
"from_date",
|
||||
"to_date",
|
||||
"column_break_97",
|
||||
@@ -1013,12 +1015,6 @@
|
||||
"label": "Print Language",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "from_date",
|
||||
@@ -1309,6 +1305,24 @@
|
||||
"fieldtype": "Time",
|
||||
"label": "Time",
|
||||
"mandatory_depends_on": "is_internal_supplier"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "auto_repeat_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ahub",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1316,7 +1330,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-25 11:46:18.748951",
|
||||
"modified": "2026-04-28 06:11:46.904768",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -159,6 +159,7 @@ class PurchaseOrder(BuyingController):
|
||||
taxes_and_charges_deducted: DF.Currency
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
title: DF.Data | None
|
||||
to_date: DF.Date | None
|
||||
total: DF.Currency
|
||||
total_net_weight: DF.Float
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"status",
|
||||
"has_unit_price_items",
|
||||
"amended_from",
|
||||
"section_break_mhyw",
|
||||
"title",
|
||||
"suppliers_section",
|
||||
"suppliers",
|
||||
"items_section",
|
||||
@@ -371,6 +373,18 @@
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Shipping Address Details",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_mhyw",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -378,7 +392,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-19 15:27:56.730649",
|
||||
"modified": "2026-04-28 06:18:05.661710",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
@@ -448,5 +462,6 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ class RequestforQuotation(BuyingController):
|
||||
suppliers: DF.Table[RequestforQuotationSupplier]
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
title: DF.Data | None
|
||||
transaction_date: DF.Date
|
||||
use_html: DF.Check
|
||||
vendor: DF.Link | None
|
||||
@@ -477,6 +478,11 @@ def create_supplier_quotation(doc):
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if frappe.session.user not in frappe.get_all(
|
||||
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
|
||||
):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
|
||||
try:
|
||||
sq_doc = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -264,6 +264,13 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
|
||||
|
||||
for data in supplier_data:
|
||||
rfq.append("suppliers", data)
|
||||
frappe.new_doc(
|
||||
"Portal User",
|
||||
user="Administrator",
|
||||
parent=data.get("supplier"),
|
||||
parentfield="portal_users",
|
||||
parenttype="Supplier",
|
||||
).insert()
|
||||
|
||||
rfq.append(
|
||||
"items",
|
||||
|
||||
@@ -126,9 +126,3 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_section",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
@@ -21,6 +20,8 @@
|
||||
"quotation_number",
|
||||
"has_unit_price_items",
|
||||
"amended_from",
|
||||
"section_break_kumc",
|
||||
"title",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -111,7 +112,7 @@
|
||||
"column_break_85",
|
||||
"select_print_heading",
|
||||
"language",
|
||||
"subscription_section",
|
||||
"auto_repeat_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"more_info",
|
||||
@@ -127,10 +128,9 @@
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"default": "{supplier_name}",
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
@@ -736,11 +736,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"fieldname": "auto_repeat",
|
||||
"fieldtype": "Link",
|
||||
@@ -940,6 +935,15 @@
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "auto_repeat_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_kumc",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -948,7 +952,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-29 21:23:13.778468",
|
||||
"modified": "2026-04-28 06:23:52.813948",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
@@ -1016,5 +1020,5 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title"
|
||||
"title_field": "supplier_name"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ class TestSupplierScorecard(ERPNextTestSuite):
|
||||
self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
|
||||
|
||||
def test_criteria_weight(self):
|
||||
delete_test_scorecards()
|
||||
my_doc = make_supplier_scorecard()
|
||||
for d in my_doc.criteria:
|
||||
d.weight = 0
|
||||
@@ -33,26 +32,6 @@ def make_supplier_scorecard():
|
||||
return my_doc
|
||||
|
||||
|
||||
def delete_test_scorecards():
|
||||
my_doc = make_supplier_scorecard()
|
||||
if frappe.db.exists("Supplier Scorecard", my_doc.name):
|
||||
# Delete all the periods, then delete the scorecard
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Period` where scorecard = %(scorecard)s""",
|
||||
{"scorecard": my_doc.name},
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.delete_doc(my_doc.doctype, my_doc.name)
|
||||
|
||||
|
||||
valid_scorecard = [
|
||||
{
|
||||
"standings": [
|
||||
|
||||
@@ -9,41 +9,15 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestSupplierScorecardCriteria(ERPNextTestSuite):
|
||||
def test_variables_exist(self):
|
||||
delete_test_scorecards()
|
||||
for d in test_good_criteria:
|
||||
frappe.get_doc(d).insert()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[0]).insert)
|
||||
|
||||
def test_formula_validate(self):
|
||||
delete_test_scorecards()
|
||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[1]).insert)
|
||||
|
||||
|
||||
def delete_test_scorecards():
|
||||
# Delete all the periods so we can delete all the criteria
|
||||
frappe.db.sql("""delete from `tabSupplier Scorecard Period`""")
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
|
||||
for d in test_good_criteria:
|
||||
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
|
||||
# Delete all the periods, then delete the scorecard
|
||||
frappe.delete_doc(d.get("doctype"), d.get("name"))
|
||||
|
||||
for d in test_bad_criteria:
|
||||
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
|
||||
# Delete all the periods, then delete the scorecard
|
||||
frappe.delete_doc(d.get("doctype"), d.get("name"))
|
||||
|
||||
|
||||
test_good_criteria = [
|
||||
{
|
||||
"name": "Delivery",
|
||||
|
||||
@@ -68,6 +68,7 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.stock.doctype.item.item import get_uom_conv_factor
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
NOT_APPLICABLE_TAX,
|
||||
ItemDetailsCtx,
|
||||
_get_item_tax_template,
|
||||
get_conversion_factor,
|
||||
@@ -1767,6 +1768,7 @@ class AccountsController(TransactionBase):
|
||||
arg.get("referenced_row"),
|
||||
arg.get("cost_center"),
|
||||
dimensions_dict,
|
||||
arg.get("project"),
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@@ -1851,6 +1853,7 @@ class AccountsController(TransactionBase):
|
||||
d.idx,
|
||||
self.cost_center,
|
||||
dimensions_dict,
|
||||
self.project,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@@ -3684,8 +3687,11 @@ def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
|
||||
|
||||
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
|
||||
tax_map = json.loads(child_item.get("item_tax_rate"))
|
||||
for tax_type in tax_map:
|
||||
tax_rate = flt(tax_map[tax_type])
|
||||
for tax_type, tax_rate in tax_map.items():
|
||||
if tax_rate == NOT_APPLICABLE_TAX:
|
||||
continue
|
||||
|
||||
tax_rate = flt(tax_rate)
|
||||
taxes = parent_doc.get("taxes") or []
|
||||
# add new row for tax head only if missing
|
||||
found = any(tax.account_head == tax_type for tax in taxes)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user