mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-25 21:55:04 +00:00
Compare commits
351 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 | ||
|
|
9312781dcd | ||
|
|
e65596fc0f | ||
|
|
bd50a0f318 | ||
|
|
dea2d21580 | ||
|
|
2353bcc3fc | ||
|
|
6e3549d185 | ||
|
|
d5143edcce | ||
|
|
63ec36a6f9 | ||
|
|
bd6269b9e7 | ||
|
|
d0bff47272 | ||
|
|
9d90fc4a84 | ||
|
|
2c292f4770 | ||
|
|
394eb93677 | ||
|
|
fbb3ccbc28 | ||
|
|
708b59b519 | ||
|
|
de08a972b6 | ||
|
|
512a35a0ab | ||
|
|
71a563428d | ||
|
|
57e458cc1e | ||
|
|
f2450eaf60 | ||
|
|
847919bf4e | ||
|
|
29be73c256 | ||
|
|
8f86a2879c | ||
|
|
eb80a3704a | ||
|
|
bfe58b2d68 | ||
|
|
39848ffb1e | ||
|
|
d2745f3ec9 | ||
|
|
3bdac5c30a | ||
|
|
a1c43ae913 | ||
|
|
93ede5b764 | ||
|
|
7d8f59eb0a | ||
|
|
66fdd061e7 | ||
|
|
b42e23993d | ||
|
|
22774fd810 | ||
|
|
4e828fd897 | ||
|
|
3cf90e804d | ||
|
|
1f5d0c58f2 | ||
|
|
720a79588d | ||
|
|
bc03f2399a | ||
|
|
abb896ecf1 | ||
|
|
c98ded52b2 | ||
|
|
03474c0589 | ||
|
|
699325506f | ||
|
|
a71814a483 | ||
|
|
3ce6dcc7a7 | ||
|
|
93def4dd13 | ||
|
|
1086a72373 | ||
|
|
379a1da254 | ||
|
|
6ec64216ce | ||
|
|
fe2161ea0c | ||
|
|
21cf83b915 | ||
|
|
62448d98de | ||
|
|
3eb58b8d36 | ||
|
|
943ddff6aa | ||
|
|
96446ed78d | ||
|
|
5866fc6cb4 | ||
|
|
60a1da0a1b | ||
|
|
f2b3adec0f | ||
|
|
233dc7c07b | ||
|
|
526c8d0418 | ||
|
|
7b0d34e979 | ||
|
|
856ba24194 | ||
|
|
9d317129f4 | ||
|
|
864a7fdab5 | ||
|
|
0bdb7e7894 | ||
|
|
e361afb6bc | ||
|
|
7b154c3069 | ||
|
|
b7f1677eef | ||
|
|
1134e25bb6 | ||
|
|
aaea4dfcc1 | ||
|
|
7b91566435 | ||
|
|
4a6fe477d4 | ||
|
|
df3f242331 | ||
|
|
7062b7153e | ||
|
|
093ca8745d | ||
|
|
52ac36ef02 | ||
|
|
9e83badbf5 | ||
|
|
7bef9542d4 | ||
|
|
6cebea314d | ||
|
|
d4fde552f4 | ||
|
|
fb1d865e9b | ||
|
|
0a257ea63d | ||
|
|
b030eeafb8 | ||
|
|
e4eb88d80b | ||
|
|
0ceb084104 | ||
|
|
31ac46ae4c | ||
|
|
901e626729 | ||
|
|
d50279b718 | ||
|
|
e1a4d9fab4 | ||
|
|
8444778f74 | ||
|
|
4c0ebee15b | ||
|
|
195a10efb3 | ||
|
|
ff104edf12 | ||
|
|
919cbd5c02 | ||
|
|
eead8d6d8c | ||
|
|
4232640a8b | ||
|
|
1237f9a0b1 | ||
|
|
1c4b2a7148 | ||
|
|
84a063a9bf | ||
|
|
5f67ef70bb | ||
|
|
c9d03d049c | ||
|
|
b91af5b2b9 | ||
|
|
ae89407840 | ||
|
|
21f36f5c21 | ||
|
|
995a29e3e1 | ||
|
|
454271ad68 | ||
|
|
ff262655bb | ||
|
|
62b83cacce | ||
|
|
e6722c84fa | ||
|
|
dc58754a60 | ||
|
|
af81ed874b | ||
|
|
14868ab98f | ||
|
|
957a5e1a65 | ||
|
|
84382db5ca | ||
|
|
273caa38d9 | ||
|
|
5719992cda | ||
|
|
2de51be5ae | ||
|
|
66ee208cb2 | ||
|
|
cd98312083 | ||
|
|
7c34cb1134 | ||
|
|
9bc0e3b2ce | ||
|
|
8941699a34 | ||
|
|
fe6a7ee7ff | ||
|
|
ab08162f34 | ||
|
|
89e3e3c59e | ||
|
|
5c3daee1bb | ||
|
|
baa714b00f | ||
|
|
bc86e2c1f2 | ||
|
|
1c0956c6e2 | ||
|
|
bd67ef8d26 | ||
|
|
01610b2fa7 | ||
|
|
d3f1bfc628 | ||
|
|
fc5a04db2e | ||
|
|
fddf9f381b | ||
|
|
5aaca83fe4 | ||
|
|
5a7d0d2765 | ||
|
|
53fc0beae5 | ||
|
|
8db397bdae | ||
|
|
e230f72e0b | ||
|
|
9386c1328a | ||
|
|
c4c76cc1b2 | ||
|
|
240d27274b | ||
|
|
e55d0ce1f8 | ||
|
|
f42a1e8a14 | ||
|
|
8674aafc86 | ||
|
|
04cced2fb5 | ||
|
|
16bd04dd97 | ||
|
|
d129e5f5bc | ||
|
|
573a1a0dcb | ||
|
|
7c503b33f8 | ||
|
|
2c81f79df7 | ||
|
|
05f47bbf6e | ||
|
|
ee61d79631 | ||
|
|
85b08e4706 | ||
|
|
013aea6b7e | ||
|
|
a8d0fb5ac9 | ||
|
|
924983794b | ||
|
|
5ade905ee8 | ||
|
|
4705f53d2c | ||
|
|
8be7793f89 | ||
|
|
0dcacad793 | ||
|
|
a478fb7131 | ||
|
|
5922d25210 | ||
|
|
6ad5e89607 | ||
|
|
6689b17b88 | ||
|
|
3592637b5c | ||
|
|
bb77018f7b | ||
|
|
f5a9657a91 | ||
|
|
7e5297a305 | ||
|
|
a8769bfb77 |
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.12.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",
|
||||
|
||||
@@ -52,60 +52,55 @@ frappe.treeview_settings["Account"] = {
|
||||
],
|
||||
root_label: "Accounts",
|
||||
get_tree_nodes: "erpnext.accounts.utils.get_children",
|
||||
on_get_node: function (nodes, deep = false) {
|
||||
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
|
||||
on_node_render: function (node, deep) {
|
||||
const render_balances = () => {
|
||||
for (let account of cur_tree.account_balance_data) {
|
||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||
if (!node || node.is_root) continue;
|
||||
|
||||
let accounts = [];
|
||||
if (deep) {
|
||||
// in case of `get_all_nodes`
|
||||
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
|
||||
} else {
|
||||
accounts = nodes;
|
||||
}
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
|
||||
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
|
||||
if (value) {
|
||||
const get_balances = frappe.call({
|
||||
method: "erpnext.accounts.utils.get_account_balances",
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company,
|
||||
include_default_fb_balances: true,
|
||||
},
|
||||
});
|
||||
|
||||
get_balances.then((r) => {
|
||||
if (!r.message || r.message.length == 0) return;
|
||||
|
||||
for (let account of r.message) {
|
||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||
if (!node || node.is_root) continue;
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? __("Dr") : __("Cr");
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
|
||||
if (account.balance !== undefined) {
|
||||
node.parent && node.parent.find(".balance-area").remove();
|
||||
$(
|
||||
'<span class="balance-area pull-right">' +
|
||||
(account.balance_in_account_currency
|
||||
? format(
|
||||
account.balance_in_account_currency,
|
||||
account.account_currency
|
||||
) + " / "
|
||||
: "") +
|
||||
format(account.balance, account.company_currency) +
|
||||
" " +
|
||||
dr_or_cr +
|
||||
"</span>"
|
||||
).insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (account.balance !== undefined) {
|
||||
node.parent && node.parent.find(".balance-area").remove();
|
||||
$(
|
||||
'<span class="balance-area pull-right">' +
|
||||
(account.account_currency != account.company_currency
|
||||
? format(account.balance_in_account_currency, account.account_currency) +
|
||||
" / "
|
||||
: "") +
|
||||
format(account.balance, account.company_currency) +
|
||||
" " +
|
||||
dr_or_cr +
|
||||
"</span>"
|
||||
).insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
|
||||
if (!cur_tree.account_balance_data) {
|
||||
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
|
||||
if (value) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.utils.get_account_balances_coa",
|
||||
args: {
|
||||
company: cur_tree.args.company,
|
||||
include_default_fb_balances: true,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.message || r.message.length === 0) return;
|
||||
cur_tree.account_balance_data = r.message || [];
|
||||
render_balances();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
render_balances();
|
||||
}
|
||||
},
|
||||
add_tree_node: "erpnext.accounts.utils.add_ac",
|
||||
menu_items: [
|
||||
|
||||
@@ -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,12 +25,29 @@
|
||||
"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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-15 03:19:47.171349",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Account",
|
||||
"link_fieldname": "account_category"
|
||||
}
|
||||
],
|
||||
"modified": "2026-03-05 06:49:34.430723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Category",
|
||||
@@ -64,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,9 +63,12 @@
|
||||
"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",
|
||||
"fetch_payment_schedule_in_payment_request",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -688,6 +692,30 @@
|
||||
"fieldname": "enable_accounting_dimensions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Enable Subscription tracking in invoice",
|
||||
"fieldname": "enable_subscription",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Subscription"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"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,
|
||||
@@ -697,7 +725,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-27 01:04:09.415288",
|
||||
"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,7 +77,9 @@ 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
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
@@ -85,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
|
||||
@@ -135,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:
|
||||
@@ -185,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
|
||||
@@ -215,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,
|
||||
@@ -224,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)
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
|
||||
"""
|
||||
Integration tests for AdvancePaymentLedgerEntry.
|
||||
Use this class for testing interactions between multiple components.
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBankReconciliationTool(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -382,7 +382,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_group": "Individual",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Poore Simon's",
|
||||
}
|
||||
@@ -413,7 +413,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_group": "Individual",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Fayva",
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBankTransactionFees(UnitTestCase):
|
||||
class TestBankTransactionFees(ERPNextTestSuite):
|
||||
def test_included_fee_throws(self):
|
||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||
withdrawal itself."""
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestExchangeRateRevaluation(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_usd_receivable_account()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.ui.form.on("Financial Report Template", {
|
||||
refresh(frm) {
|
||||
if (frm.is_new() || frm.doc.rows.length === 0) return;
|
||||
|
||||
// add custom button to view missed accounts
|
||||
frm.add_custom_button(__("View Account Coverage"), function () {
|
||||
let selected_rows = frm.get_field("rows").grid.get_selected_children();
|
||||
@@ -20,7 +22,7 @@ frappe.ui.form.on("Financial Report Template", {
|
||||
});
|
||||
},
|
||||
|
||||
validate(frm) {
|
||||
after_save(frm) {
|
||||
if (!frm.doc.rows || frm.doc.rows.length === 0) {
|
||||
frappe.msgprint(__("At least one row is required for a financial report template"));
|
||||
}
|
||||
@@ -34,14 +36,6 @@ frappe.ui.form.on("Financial Report Row", {
|
||||
update_formula_label(frm, row.data_source);
|
||||
update_formula_description(frm, row.data_source);
|
||||
|
||||
if (row.data_source !== "Account Data") {
|
||||
frappe.model.set_value(cdt, cdn, "balance_type", "");
|
||||
}
|
||||
|
||||
if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) {
|
||||
frappe.model.set_value(cdt, cdn, "calculation_formula", "");
|
||||
}
|
||||
|
||||
set_up_filters_editor(frm, cdt, cdn);
|
||||
},
|
||||
|
||||
@@ -322,6 +316,8 @@ function update_formula_description(frm, data_source) {
|
||||
const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
|
||||
const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
|
||||
const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
|
||||
const code_style = `style="background: var(--bg-light-gray); padding: var(--padding-xs); border-radius: var(--border-radius); font-size: 0.85em; width: max-content; margin-bottom: var(--margin-sm);"`;
|
||||
const pre_style = `style="margin: 0; border-radius: var(--border-radius)"`;
|
||||
|
||||
let description_html = "";
|
||||
|
||||
@@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
|
||||
<li><code>my_app.financial_reports.get_kpi_data</code></li>
|
||||
</ul>
|
||||
|
||||
<h6 ${subtitle_style}>Method Signature:</h6>
|
||||
<div ${code_style}>
|
||||
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br> # filters: dict — report filters (company, period, etc.) <br> # periods: list[dict] — period definitions <br> # row: dict — the current report row <br><br> return [1000.0, 1200.0, 1150.0] # one value per period</pre>
|
||||
</div>
|
||||
|
||||
<h6 ${subtitle_style}>Return Format:</h6>
|
||||
<p ${text_style}>Numbers for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
|
||||
<p ${text_style}>A list of numbers, one for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
|
||||
</div>`;
|
||||
} else if (data_source === "Blank Line") {
|
||||
description_html = `
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:template_name",
|
||||
"creation": "2025-08-02 04:44:15.184541",
|
||||
"doctype": "DocType",
|
||||
@@ -31,7 +30,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Report Type",
|
||||
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement"
|
||||
"options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:frappe.boot.developer_mode",
|
||||
@@ -66,7 +66,7 @@
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-14 00:11:03.508139",
|
||||
"modified": "2026-02-23 01:04:05.797161",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Financial Report Template",
|
||||
|
||||
@@ -32,6 +32,19 @@ class FinancialReportTemplate(Document):
|
||||
template_name: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.clear_hidden_fields()
|
||||
|
||||
def clear_hidden_fields(self):
|
||||
style_data_sources = {"Blank Line", "Column Break", "Section Break"}
|
||||
|
||||
for row in self.rows:
|
||||
if row.data_source != "Account Data":
|
||||
row.balance_type = None
|
||||
|
||||
if row.data_source in style_data_sources:
|
||||
row.calculation_formula = None
|
||||
|
||||
def validate(self):
|
||||
validator = TemplateValidator(self)
|
||||
result = validator.validate()
|
||||
|
||||
@@ -70,8 +70,8 @@ class ValidationResult:
|
||||
self.warnings.append(issue)
|
||||
|
||||
def notify_user(self) -> None:
|
||||
warnings = "<br><br>".join(str(w) for w in self.warnings)
|
||||
errors = "<br><br>".join(str(e) for e in self.issues)
|
||||
warnings = "<br><br>".join(str(w) for w in self.warnings if w)
|
||||
errors = "<br><br>".join(str(e) for e in self.issues if e)
|
||||
|
||||
if warnings:
|
||||
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
|
||||
@@ -99,9 +99,8 @@ class TemplateValidator:
|
||||
result.merge(validator.validate(self.template))
|
||||
|
||||
# Run row-level validations
|
||||
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
|
||||
for row in self.template.rows:
|
||||
result.merge(self.formula_validator.validate(row, account_fields))
|
||||
result.merge(self.formula_validator.validate(row))
|
||||
|
||||
return result
|
||||
|
||||
@@ -383,7 +382,8 @@ class AccountFilterValidator(Validator):
|
||||
"""Validates account filter expressions used in Account Data rows"""
|
||||
|
||||
def __init__(self, account_fields: set | None = None):
|
||||
self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns)
|
||||
self.account_meta = frappe.get_meta("Account")
|
||||
self.account_fields = account_fields or set(self.account_meta._valid_columns)
|
||||
|
||||
def validate(self, row) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
@@ -403,7 +403,11 @@ class AccountFilterValidator(Validator):
|
||||
|
||||
try:
|
||||
filter_config = json.loads(row.calculation_formula)
|
||||
error = self._validate_filter_structure(filter_config, self.account_fields)
|
||||
error = self._validate_filter_structure(
|
||||
filter_config,
|
||||
self.account_fields,
|
||||
row.advanced_filtering,
|
||||
)
|
||||
|
||||
if error:
|
||||
result.add_error(
|
||||
@@ -425,7 +429,12 @@ class AccountFilterValidator(Validator):
|
||||
|
||||
return result
|
||||
|
||||
def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None:
|
||||
def _validate_filter_structure(
|
||||
self,
|
||||
filter_config,
|
||||
account_fields: set,
|
||||
advanced_filtering: bool = False,
|
||||
) -> str | None:
|
||||
# simple condition: [field, operator, value]
|
||||
if isinstance(filter_config, list):
|
||||
if len(filter_config) != 3:
|
||||
@@ -436,8 +445,10 @@ class AccountFilterValidator(Validator):
|
||||
if not isinstance(field, str) or not isinstance(operator, str):
|
||||
return "Field and operator must be strings"
|
||||
|
||||
display = (field if advanced_filtering else self.account_meta.get_label(field)) or field
|
||||
|
||||
if field not in account_fields:
|
||||
return f"Field '{field}' is not a valid account field"
|
||||
return f"Field '{display}' is not a valid Account field"
|
||||
|
||||
if operator.casefold() not in OPERATOR_MAP:
|
||||
return f"Invalid operator '{operator}'"
|
||||
@@ -460,7 +471,7 @@ class AccountFilterValidator(Validator):
|
||||
|
||||
# recursive
|
||||
for condition in conditions:
|
||||
error = self._validate_filter_structure(condition, account_fields)
|
||||
error = self._validate_filter_structure(condition, account_fields, advanced_filtering)
|
||||
if error:
|
||||
return error
|
||||
else:
|
||||
@@ -476,7 +487,7 @@ class FormulaValidator(Validator):
|
||||
self.calculation_validator = CalculationFormulaValidator(reference_codes)
|
||||
self.account_filter_validator = AccountFilterValidator()
|
||||
|
||||
def validate(self, row, account_fields: set) -> ValidationResult:
|
||||
def validate(self, row) -> ValidationResult:
|
||||
result = ValidationResult()
|
||||
|
||||
if not row.calculation_formula:
|
||||
@@ -486,9 +497,6 @@ class FormulaValidator(Validator):
|
||||
return self.calculation_validator.validate(row)
|
||||
|
||||
elif row.data_source == "Account Data":
|
||||
# Update account fields if provided
|
||||
if account_fields:
|
||||
self.account_filter_validator.account_fields = account_fields
|
||||
return self.account_filter_validator.validate(row)
|
||||
|
||||
elif row.data_source == "Custom API":
|
||||
|
||||
@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine
|
||||
DependencyResolver,
|
||||
FilterExpressionParser,
|
||||
FinancialQueryBuilder,
|
||||
FinancialReportEngine,
|
||||
FormulaCalculator,
|
||||
PeriodValue,
|
||||
)
|
||||
@@ -1295,6 +1296,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
|
||||
self.data_source = "Account Data"
|
||||
self.idx = 1
|
||||
self.reverse_sign = 0
|
||||
self.advanced_filtering = True
|
||||
|
||||
return MockReportRow(formula, reference_code)
|
||||
|
||||
@@ -1951,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"
|
||||
@@ -2024,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
|
||||
@@ -353,8 +354,11 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Account {0} should be of type Expense").format(d.account))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
if self.voucher_type == "Periodic Accounting Entry":
|
||||
# Skip validation for periodic accounting entry
|
||||
if (
|
||||
not erpnext.is_perpetual_inventory_enabled(self.company)
|
||||
or self.voucher_type == "Periodic Accounting Entry"
|
||||
):
|
||||
# Skip validation for periodic accounting entry and Perpetual Inventory Disabled Company.
|
||||
return
|
||||
|
||||
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||
@@ -1023,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:
|
||||
@@ -1538,31 +1542,31 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not frappe.db.has_column("Journal Entry", searchfield):
|
||||
return []
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT jv.name, jv.posting_date, jv.user_remark
|
||||
FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail
|
||||
WHERE jv_detail.parent = jv.name
|
||||
AND jv_detail.account = %(account)s
|
||||
AND IFNULL(jv_detail.party, '') = %(party)s
|
||||
AND (
|
||||
jv_detail.reference_type IS NULL
|
||||
OR jv_detail.reference_type = ''
|
||||
)
|
||||
AND jv.docstatus = 1
|
||||
AND jv.`{searchfield}` LIKE %(txt)s
|
||||
ORDER BY jv.name DESC
|
||||
LIMIT %(limit)s offset %(offset)s
|
||||
""",
|
||||
dict(
|
||||
account=filters.get("account"),
|
||||
party=cstr(filters.get("party")),
|
||||
txt=f"%{txt}%",
|
||||
offset=start,
|
||||
limit=page_len,
|
||||
),
|
||||
JournalEntry = frappe.qb.DocType("Journal Entry")
|
||||
JournalEntryAccount = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(JournalEntry)
|
||||
.join(JournalEntryAccount)
|
||||
.on(JournalEntryAccount.parent == JournalEntry.name)
|
||||
.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)
|
||||
.where(JournalEntry[searchfield].like(f"%{txt}%"))
|
||||
.orderby(JournalEntry.name, order=frappe.qb.desc)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
party = filters.get("party")
|
||||
if party:
|
||||
query = query.where(JournalEntryAccount.party == party)
|
||||
else:
|
||||
query = query.where(JournalEntryAccount.party.isnull() | (JournalEntryAccount.party == ""))
|
||||
|
||||
return query.run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_outstanding(args):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,7 +10,7 @@ from erpnext.accounts.utils import run_ledger_health_checks
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestLedgerHealth(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +70,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
});
|
||||
});
|
||||
|
||||
if (frm.doc.create_missing_party) {
|
||||
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
|
||||
}
|
||||
frm.trigger("update_party_labels");
|
||||
},
|
||||
|
||||
setup_company_filters: function (frm) {
|
||||
@@ -127,7 +125,9 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
frappe.model.set_value(row.doctype, row.name, "party", "");
|
||||
frappe.model.set_value(row.doctype, row.name, "party_name", "");
|
||||
});
|
||||
frm.clear_table("invoices");
|
||||
frm.refresh_fields();
|
||||
frm.trigger("update_party_labels");
|
||||
},
|
||||
|
||||
make_dashboard: function (frm) {
|
||||
@@ -175,6 +175,32 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
}
|
||||
frm.refresh_field("invoices");
|
||||
},
|
||||
|
||||
update_party_labels: function (frm) {
|
||||
let is_sales = frm.doc.invoice_type == "Sales";
|
||||
|
||||
frm.fields_dict["invoices"].grid.update_docfield_property(
|
||||
"party",
|
||||
"label",
|
||||
is_sales ? "Customer ID" : "Supplier ID"
|
||||
);
|
||||
frm.fields_dict["invoices"].grid.update_docfield_property(
|
||||
"party_name",
|
||||
"label",
|
||||
is_sales ? "Customer Name" : "Supplier Name"
|
||||
);
|
||||
|
||||
frm.set_df_property(
|
||||
"create_missing_party",
|
||||
"description",
|
||||
is_sales
|
||||
? __("If party does not exist, create it using the Customer Name field.")
|
||||
: __("If party does not exist, create it using the Supplier Name field.")
|
||||
);
|
||||
|
||||
frm.refresh_field("invoices");
|
||||
frm.refresh_field("create_missing_party");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Opening Invoice Creation Tool Item", {
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_ynel",
|
||||
"company",
|
||||
"create_missing_party",
|
||||
"column_break_3",
|
||||
"invoice_type",
|
||||
"create_missing_party",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -25,11 +26,11 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If party does not exist, create it using the Party Name field.",
|
||||
"fieldname": "create_missing_party",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Missing Party"
|
||||
@@ -79,12 +80,17 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ynel",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-23 00:32:15.600086",
|
||||
"modified": "2026-03-31 01:47:20.360352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
|
||||
@@ -180,7 +180,7 @@ def make_customer(customer=None):
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": customer_name,
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_group": "Individual",
|
||||
"customer_type": "Company",
|
||||
"territory": "All Territories",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,7 +14,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProcessStatementOfAccounts(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
||||
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
|
||||
|
||||
@@ -21,10 +21,12 @@ frappe.ui.form.on("Promotional Scheme", {
|
||||
|
||||
selling: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.toggle_enable("buying", !frm.doc.selling);
|
||||
},
|
||||
|
||||
buying: function (frm) {
|
||||
frm.trigger("set_options_for_applicable_for");
|
||||
frm.toggle_enable("selling", !frm.doc.buying);
|
||||
},
|
||||
|
||||
set_options_for_applicable_for: function (frm) {
|
||||
|
||||
@@ -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
|
||||
@@ -332,9 +333,6 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.bill_date:
|
||||
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
|
||||
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
if not self.credit_to:
|
||||
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
|
||||
@@ -983,6 +981,10 @@ class PurchaseInvoice(BuyingController):
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
self.get_provisional_accounts()
|
||||
|
||||
adjust_incoming_rate = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||
)
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
if item.item_code:
|
||||
@@ -1161,7 +1163,11 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
# check if the exchange rate has changed
|
||||
if item.get("purchase_receipt") and self.auto_accounting_for_stock:
|
||||
if (
|
||||
not adjust_incoming_rate
|
||||
and item.get("purchase_receipt")
|
||||
and self.auto_accounting_for_stock
|
||||
):
|
||||
if (
|
||||
exchange_rate_map[item.purchase_receipt]
|
||||
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
|
||||
@@ -1198,6 +1204,7 @@ class PurchaseInvoice(BuyingController):
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
self.auto_accounting_for_stock
|
||||
and self.is_opening == "No"
|
||||
|
||||
@@ -350,6 +350,12 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
)
|
||||
|
||||
original_value = frappe.db.get_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse="Stores - TCP1",
|
||||
@@ -368,14 +374,19 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
# fetching the latest GL Entry with exchange gain and loss account account
|
||||
amount = frappe.db.get_value(
|
||||
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "credit"
|
||||
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
|
||||
)
|
||||
|
||||
discrepancy_caused_by_exchange_rate_diff = abs(
|
||||
pi.items[0].base_net_amount - pr.items[0].base_net_amount
|
||||
)
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
|
||||
)
|
||||
|
||||
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice,
|
||||
@@ -2245,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(
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -206,7 +207,8 @@
|
||||
{
|
||||
"fieldname": "rejected_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Rejected Qty"
|
||||
"label": "Rejected Qty",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
@@ -226,6 +228,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -261,14 +264,16 @@
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "discount_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Discount on Price List Rate (%)"
|
||||
"label": "Discount on Price List Rate (%)",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "price_list_rate",
|
||||
"fieldname": "discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break3",
|
||||
@@ -401,12 +406,14 @@
|
||||
{
|
||||
"fieldname": "weight_per_unit",
|
||||
"fieldtype": "Float",
|
||||
"label": "Weight Per Unit"
|
||||
"label": "Weight Per Unit",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_weight",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Weight",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -417,7 +424,8 @@
|
||||
"fieldname": "weight_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Weight UOM",
|
||||
"options": "UOM"
|
||||
"options": "UOM",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
@@ -429,7 +437,8 @@
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Accepted Warehouse",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rejected_warehouse",
|
||||
@@ -674,7 +683,8 @@
|
||||
"fieldname": "asset_location",
|
||||
"fieldtype": "Link",
|
||||
"label": "Asset Location",
|
||||
"options": "Location"
|
||||
"options": "Location",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "po_detail",
|
||||
@@ -730,7 +740,6 @@
|
||||
"label": "Valuation Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -796,6 +805,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Asset Category",
|
||||
"options": "Asset Category",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -828,6 +838,7 @@
|
||||
"label": "Rate of Stock UOM",
|
||||
"no_copy": 1,
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -866,6 +877,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate With Margin",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -892,7 +904,8 @@
|
||||
"default": "1",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider for Tax Withholding"
|
||||
"label": "Consider for Tax Withholding",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||
@@ -918,7 +931,8 @@
|
||||
"fieldname": "wip_composite_asset",
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Composite Asset",
|
||||
"options": "Asset"
|
||||
"options": "Asset",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
||||
@@ -930,7 +944,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
"label": "Use Serial No / Batch Fields",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
@@ -977,7 +992,8 @@
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_category",
|
||||
@@ -991,7 +1007,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-15 21:07:49.455930",
|
||||
"modified": "2026-04-07 15:40:45.687554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -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}%")})
|
||||
|
||||
|
||||
@@ -9,29 +9,25 @@ from frappe.utils import add_days, nowdate, today
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestRepostAccountingLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
|
||||
update_repost_settings()
|
||||
|
||||
def test_01_basic_functions(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
@@ -48,7 +44,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
# Test Validation Error
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append(
|
||||
@@ -65,7 +61,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
ral.save()
|
||||
|
||||
# manually set an incorrect debit amount in DB
|
||||
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to})
|
||||
gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": "Debtors - _TC"})
|
||||
frappe.db.set_value("GL Entry", gle[0], "debit", 90)
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
@@ -94,23 +90,23 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
def test_02_deferred_accounting_valiations(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
si.items[0].enable_deferred_revenue = True
|
||||
si.items[0].deferred_revenue_account = self.deferred_revenue
|
||||
si.items[0].deferred_revenue_account = "Deferred Revenue - _TC"
|
||||
si.items[0].service_start_date = nowdate()
|
||||
si.items[0].service_end_date = add_days(nowdate(), 90)
|
||||
si.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
@@ -118,35 +114,35 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
def test_04_pcv_validation(self):
|
||||
# Clear old GL entries so PCV can be submitted.
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
qb.from_(gl).delete().where(gl.company == self.company).run()
|
||||
qb.from_(gl).delete().where(gl.company == "_Test Company").run()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
fy = get_fiscal_year(today(), company=self.company)
|
||||
fy = get_fiscal_year(today(), company="_Test Company")
|
||||
pcv = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Period Closing Voucher",
|
||||
"transaction_date": today(),
|
||||
"period_start_date": fy[1],
|
||||
"period_end_date": today(),
|
||||
"company": self.company,
|
||||
"company": "_Test Company",
|
||||
"fiscal_year": fy[0],
|
||||
"cost_center": self.cost_center,
|
||||
"closing_account_head": self.retained_earnings,
|
||||
"cost_center": "Main - _TC",
|
||||
"closing_account_head": "Retained Earnings - _TC",
|
||||
"remarks": "test",
|
||||
}
|
||||
)
|
||||
pcv.save().submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
self.assertRaises(frappe.ValidationError, ral.save)
|
||||
|
||||
@@ -156,12 +152,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
def test_03_deletion_flag_and_preview_function(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
@@ -170,7 +166,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
# with deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.delete_cancelled_entries = True
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
@@ -181,12 +177,12 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
def test_05_without_deletion_flag(self):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
item="_Test Item",
|
||||
company="_Test Company",
|
||||
customer="_Test Customer",
|
||||
debit_to="Debtors - _TC",
|
||||
parent_cost_center="Main - _TC",
|
||||
cost_center="Main - _TC",
|
||||
rate=100,
|
||||
)
|
||||
|
||||
@@ -195,7 +191,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
# without deletion flag set
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = self.company
|
||||
ral.company = "_Test Company"
|
||||
ral.delete_cancelled_entries = False
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
|
||||
@@ -207,19 +203,24 @@ class TestRepostAccountingLedger(AccountsTestMixin, 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",
|
||||
company=self.company,
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
another_provisional_account = create_account(
|
||||
account_name="Another Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company=self.company,
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
@@ -229,7 +230,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
item = make_item(properties={"is_stock_item": 0})
|
||||
|
||||
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
|
||||
pr = make_purchase_receipt(company="_Test Company", item_code=item.name, rate=1000.0, qty=1.0)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
@@ -246,7 +247,7 @@ class TestRepostAccountingLedger(AccountsTestMixin, ERPNextTestSuite):
|
||||
)
|
||||
|
||||
repost_doc = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_doc.company = self.company
|
||||
repost_doc.company = "_Test Company"
|
||||
repost_doc.delete_cancelled_entries = True
|
||||
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
|
||||
repost_doc.save().submit()
|
||||
@@ -279,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),
|
||||
@@ -165,13 +167,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Show buttons only when pos view is active
|
||||
if (cint(doc.docstatus == 0) && this.frm.page.current_view_name !== "pos" && !doc.is_return) {
|
||||
this.frm.cscript.sales_order_btn();
|
||||
this.frm.cscript.delivery_note_btn();
|
||||
this.frm.cscript.quotation_btn();
|
||||
}
|
||||
this.toggle_get_items();
|
||||
|
||||
this.set_default_print_format();
|
||||
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
|
||||
@@ -260,6 +256,93 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
}
|
||||
|
||||
toggle_get_items() {
|
||||
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
|
||||
|
||||
buttons.forEach((label) => {
|
||||
this.frm.remove_custom_button(label, "Get Items From");
|
||||
});
|
||||
|
||||
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.frm.doc.is_return) {
|
||||
this.frm.cscript.sales_order_btn();
|
||||
this.frm.cscript.quotation_btn();
|
||||
this.frm.cscript.timesheet_btn();
|
||||
}
|
||||
|
||||
this.frm.cscript.delivery_note_btn();
|
||||
}
|
||||
|
||||
timesheet_btn() {
|
||||
var me = this;
|
||||
|
||||
me.frm.add_custom_button(
|
||||
__("Timesheet"),
|
||||
function () {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Item Code"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {
|
||||
is_sales_item: 1,
|
||||
customer: me.frm.doc.customer,
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: me.frm.doc.project,
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const data = d.get_values();
|
||||
me.frm.events.add_timesheet_data(me.frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
item_code: data.item_code,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
sales_order_btn() {
|
||||
var me = this;
|
||||
|
||||
@@ -331,6 +414,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
this.$delivery_note_btn = this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
function () {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer"),
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_doctype: "Delivery Note",
|
||||
@@ -343,7 +432,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
var filters = {
|
||||
docstatus: 1,
|
||||
company: me.frm.doc.company,
|
||||
is_return: 0,
|
||||
is_return: me.frm.doc.is_return,
|
||||
};
|
||||
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
|
||||
return {
|
||||
@@ -465,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() {
|
||||
@@ -610,6 +701,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
apply_tds(frm) {
|
||||
this.frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
|
||||
is_return() {
|
||||
this.toggle_get_items();
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
@@ -1061,71 +1156,6 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
|
||||
frm.add_custom_button(
|
||||
__("Timesheet"),
|
||||
function () {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Item Code"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {
|
||||
is_sales_item: 1,
|
||||
customer: frm.doc.customer,
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: frm.doc.project,
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const data = d.get_values();
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
item_code: data.item_code,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.is_debit_note) {
|
||||
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1101,9 +1102,6 @@ class SalesInvoice(SellingController):
|
||||
if self.po_date:
|
||||
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
|
||||
|
||||
else:
|
||||
self.remarks = _("No Remarks")
|
||||
|
||||
def validate_auto_set_posting_time(self):
|
||||
# Don't auto set the posting date and time if invoice is amended
|
||||
if self.is_new() and self.amended_from:
|
||||
|
||||
@@ -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"
|
||||
@@ -2887,7 +2883,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.submit()
|
||||
|
||||
# Check if adjustment entry is created
|
||||
self.assertTrue(
|
||||
self.assertFalse(
|
||||
frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -310,7 +311,8 @@
|
||||
"fieldname": "discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
|
||||
@@ -853,6 +855,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate of Stock UOM",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -869,6 +872,7 @@
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -926,7 +930,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "use_serial_batch_fields",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Serial No / Batch Fields"
|
||||
"label": "Use Serial No / Batch Fields",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
@@ -941,7 +946,8 @@
|
||||
"fieldname": "distributed_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "available_quantity_section",
|
||||
@@ -1010,7 +1016,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-23 14:37:14.853941",
|
||||
"modified": "2026-02-24 14:37:16.853941",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice 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"):
|
||||
|
||||
@@ -128,6 +128,7 @@ class TaxWithholdingDetails:
|
||||
self.party_type = party_type
|
||||
self.party = party
|
||||
self.company = company
|
||||
self.tax_id = get_tax_id_for_party(self.party_type, self.party)
|
||||
|
||||
def get(self) -> list:
|
||||
"""
|
||||
@@ -161,6 +162,7 @@ class TaxWithholdingDetails:
|
||||
disable_cumulative_threshold=doc.disable_cumulative_threshold,
|
||||
disable_transaction_threshold=doc.disable_transaction_threshold,
|
||||
taxable_amount=0,
|
||||
tax_id=self.tax_id,
|
||||
)
|
||||
|
||||
# ldc (only if valid based on posting date)
|
||||
@@ -181,17 +183,13 @@ class TaxWithholdingDetails:
|
||||
if self.party_type != "Supplier":
|
||||
return ldc_details
|
||||
|
||||
# NOTE: This can be a configurable option
|
||||
# To check if filter by tax_id is needed
|
||||
tax_id = get_tax_id_for_party(self.party_type, self.party)
|
||||
|
||||
# ldc details
|
||||
ldc_records = self.get_valid_ldc_records(tax_id)
|
||||
ldc_records = self.get_valid_ldc_records(self.tax_id)
|
||||
if not ldc_records:
|
||||
return ldc_details
|
||||
|
||||
ldc_names = [ldc.name for ldc in ldc_records]
|
||||
ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, tax_id)
|
||||
ldc_utilization_map = self.get_ldc_utilization_by_category(ldc_names, self.tax_id)
|
||||
|
||||
# map
|
||||
for ldc in ldc_records:
|
||||
@@ -254,4 +252,5 @@ class TaxWithholdingDetails:
|
||||
|
||||
@allow_regional
|
||||
def get_tax_id_for_party(party_type, party):
|
||||
return None
|
||||
# cannot use tax_id from doc because payment and journal entry do not have tax_id field.\
|
||||
return frappe.db.get_value(party_type, party, "tax_id")
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# See license.txt
|
||||
|
||||
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
|
||||
@@ -1921,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]:
|
||||
@@ -1947,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
|
||||
@@ -1971,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,
|
||||
@@ -2042,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):
|
||||
"""
|
||||
@@ -3541,6 +3686,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
|
||||
entry.withholding_amount = 5001 # Should be 5000 (10% of 50000)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Withholding Amount.*does not match", pi.save)
|
||||
|
||||
def test_tax_id_is_set_in_all_generated_entries_from_party_doctype(self):
|
||||
self.setup_party_with_category("Supplier", "Test TDS Supplier3", "New TDS Category")
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_id", "ABCTY1234D")
|
||||
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=40000)
|
||||
pi.submit()
|
||||
|
||||
entries = frappe.get_all(
|
||||
"Tax Withholding Entry",
|
||||
filters={"parenttype": "Purchase Invoice", "parent": pi.name},
|
||||
fields=["name", "tax_id"],
|
||||
)
|
||||
|
||||
self.assertTrue(entries)
|
||||
self.assertTrue(all(entry.tax_id == "ABCTY1234D" for entry in entries))
|
||||
|
||||
def test_threshold_considers_two_parties_with_same_tax_id_with_overrided_hook(self):
|
||||
self.setup_party_with_category("Supplier", "Test TDS Supplier1", "Cumulative Threshold TDS")
|
||||
self.setup_party_with_category("Supplier", "Test TDS Supplier2", "Cumulative Threshold TDS")
|
||||
|
||||
with patch(
|
||||
"erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category.get_tax_id_for_party",
|
||||
return_value="AAAPL1234C",
|
||||
):
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier1", rate=20000)
|
||||
pi1.submit()
|
||||
|
||||
pi2 = create_purchase_invoice(supplier="Test TDS Supplier2", rate=20000)
|
||||
|
||||
pi2.submit()
|
||||
|
||||
entries = frappe.get_all(
|
||||
"Tax Withholding Entry",
|
||||
filters={"parenttype": "Purchase Invoice", "parent": pi2.name},
|
||||
fields=["status", "withholding_amount"],
|
||||
)
|
||||
|
||||
self.assertEqual(len(entries), 1)
|
||||
self.assertEqual(entries[0].status, "Settled")
|
||||
self.assertEqual(entries[0].withholding_amount, 2000.0)
|
||||
|
||||
|
||||
def create_purchase_invoice(**args):
|
||||
# return sales invoice doc object
|
||||
@@ -3955,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,
|
||||
@@ -3976,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(
|
||||
|
||||
@@ -344,7 +344,6 @@ class TaxWithholdingEntry(Document):
|
||||
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
TaxWithholdingDetails,
|
||||
get_tax_id_for_party,
|
||||
)
|
||||
|
||||
|
||||
@@ -641,13 +640,17 @@ 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)
|
||||
)
|
||||
|
||||
# NOTE: This can be a configurable option
|
||||
# To check if filter by tax_id is needed
|
||||
tax_id = get_tax_id_for_party(self.party_type, self.party)
|
||||
query = query.where(entry.tax_id == tax_id) if tax_id else query.where(entry.party == self.party)
|
||||
query = (
|
||||
query.where(entry.tax_id == category.tax_id)
|
||||
if category.tax_id
|
||||
else query.where(entry.party == self.party)
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
@@ -686,6 +689,7 @@ class TaxWithholdingController:
|
||||
"company": self.doc.company,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"tax_id": category.tax_id,
|
||||
"tax_withholding_category": category.name,
|
||||
"tax_withholding_group": category.tax_withholding_group,
|
||||
"tax_rate": category.tax_rate,
|
||||
@@ -1052,6 +1056,7 @@ class TaxWithholdingController:
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"company": self.doc.company,
|
||||
"tax_id": category.tax_id,
|
||||
}
|
||||
)
|
||||
return entry
|
||||
|
||||
@@ -14,7 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestUnreconcilePayment(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAccountsPayable(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
@@ -117,3 +117,49 @@ class TestAccountsPayable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
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;
|
||||
"""
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
@@ -120,12 +120,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||
self.assertFalse(row.get("remarks"))
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
@@ -178,11 +178,11 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
@@ -225,9 +225,10 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
expected_data = [8000, 8000, "No Remarks"] # Data in company currency
|
||||
expected_data = [8000, 8000] # Data in company currency
|
||||
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||
self.assertFalse(row.get("remarks"))
|
||||
|
||||
# CASE 2: Transaction currency and party account currency are the same
|
||||
self.create_customer(
|
||||
@@ -258,18 +259,20 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
expected_data = [100, 100, "No Remarks"] # Data in Part Account Currency
|
||||
expected_data = [100, 100] # Data in Part Account Currency
|
||||
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||
self.assertFalse(row.get("remarks"))
|
||||
|
||||
# View in Company currency
|
||||
filters.pop("in_party_currency")
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
expected_data = [8000, 8000, "No Remarks"] # Data in Company Currency
|
||||
expected_data = [8000, 8000] # Data in Company Currency
|
||||
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||
self.assertFalse(row.get("remarks"))
|
||||
|
||||
def test_accounts_receivable_with_partial_payment(self):
|
||||
filters = {
|
||||
@@ -285,11 +288,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
|
||||
expected_data = [[200, 60], [200, 100], [200, 40]]
|
||||
|
||||
for i in range(3):
|
||||
row = report[1][i - 1]
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
||||
self.assertFalse(row.get("remarks"))
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
self.create_payment_entry(si.name)
|
||||
@@ -348,11 +352,12 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [100, 100, "No Remarks"]
|
||||
expected_data = [100, 100]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
|
||||
self.assertFalse(row.get("remarks"))
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
self.create_payment_entry(si.name)
|
||||
@@ -769,22 +774,18 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
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
|
||||
@@ -992,22 +993,18 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
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
|
||||
@@ -1199,3 +1196,52 @@ class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
|
||||
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,7 +8,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.create_company()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCustomerLedgerSummary(AccountsTestMixin, ERPNextTestSuite):
|
||||
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -142,6 +142,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
|
||||
total = 0
|
||||
row = {
|
||||
"account": d.name,
|
||||
"is_group": d.is_group,
|
||||
"parent_account": d.parent_account,
|
||||
"indent": d.indent,
|
||||
"from_date": filters.from_date,
|
||||
|
||||
@@ -74,6 +74,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
label: __("Party"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "party_type",
|
||||
depends_on: "party_type",
|
||||
get_data: function (txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
|
||||
@@ -578,7 +578,11 @@ class GrossProfitGenerator:
|
||||
|
||||
# get buying rate
|
||||
if flt(row.qty):
|
||||
row.buying_rate = flt(row.buying_amount / flt(row.qty), self.float_precision)
|
||||
row.buying_rate = (
|
||||
flt(row.buying_amount / flt(row.qty), self.float_precision)
|
||||
if not row.delivered_by_supplier
|
||||
else None
|
||||
)
|
||||
row.base_rate = flt(row.base_amount / flt(row.qty), self.float_precision)
|
||||
else:
|
||||
if self.is_not_invoice_row(row):
|
||||
@@ -630,7 +634,8 @@ class GrossProfitGenerator:
|
||||
returned_item_row.qty += row.qty
|
||||
returned_item_row.base_amount += row.base_amount
|
||||
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
if not row.delivered_by_supplier:
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
|
||||
def get_average_rate_based_on_group_by(self):
|
||||
for key in list(self.grouped):
|
||||
@@ -769,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
|
||||
@@ -951,6 +977,7 @@ class GrossProfitGenerator:
|
||||
SalesInvoice.is_return,
|
||||
SalesInvoiceItem.cost_center,
|
||||
SalesInvoiceItem.serial_and_batch_bundle,
|
||||
SalesInvoiceItem.delivered_by_supplier,
|
||||
)
|
||||
|
||||
if self.filters.group_by == "Sales Person":
|
||||
|
||||
@@ -731,6 +731,31 @@ class TestGrossProfit(ERPNextTestSuite):
|
||||
self.assertEqual(total[7], 1000.0)
|
||||
self.assertEqual(total[8], 100.0)
|
||||
|
||||
def test_drop_ship(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order, make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item("_Test Drop Ship Item", properties={"is_stock_item": 1, "delivered_by_supplier": 1})
|
||||
|
||||
so = make_sales_order(item=item.name, qty=10, rate=100)
|
||||
po = make_purchase_order(so.name, selected_items=[so.items[0]])[0]
|
||||
po.items[0].rate = 80
|
||||
po.supplier = "_Test Supplier"
|
||||
po.submit()
|
||||
make_purchase_invoice(po.name).submit()
|
||||
si = make_sales_invoice(so.name).submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=si.company, from_date=si.posting_date, to_date=si.posting_date, group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
self.assertEqual(data[1].buying_amount, 800)
|
||||
self.assertIsNone(data[1].buying_rate)
|
||||
self.assertEqual(data[1]["gross_profit_%"], 20)
|
||||
|
||||
|
||||
def make_sales_person(sales_person_name="_Test Sales Person"):
|
||||
if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user