mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-16 11:48:48 +00:00
Compare commits
322 Commits
mariadb_li
...
pot_develo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
945b390d25 | ||
|
|
ac448988ca | ||
|
|
cc48cfaa5d | ||
|
|
46e6e48495 | ||
|
|
27e5344188 | ||
|
|
ed79adebc4 | ||
|
|
d45d20e4db | ||
|
|
f41c6c037b | ||
|
|
313913b329 | ||
|
|
56ddb16186 | ||
|
|
4f1acc9349 | ||
|
|
ecff9dfdd8 | ||
|
|
73d2878e08 | ||
|
|
feaf39a812 | ||
|
|
1662b7c311 | ||
|
|
4edbe77f67 | ||
|
|
fa228da29c | ||
|
|
9aea4ba51a | ||
|
|
b7039cc506 | ||
|
|
1e44e3c1f6 | ||
|
|
a8e2386daa | ||
|
|
7e12332ea5 | ||
|
|
03d6550db3 | ||
|
|
b1311ceb30 | ||
|
|
5089cf2155 | ||
|
|
204de4934a | ||
|
|
8f3ed909c3 | ||
|
|
1728a95111 | ||
|
|
4174269091 | ||
|
|
cf6913891a | ||
|
|
7591656491 | ||
|
|
766c5bbe2b | ||
|
|
446264e496 | ||
|
|
e01ff50833 | ||
|
|
8a97b39028 | ||
|
|
f6e16c1180 | ||
|
|
dd23d4c81b | ||
|
|
dfd115cee5 | ||
|
|
37a964c300 | ||
|
|
98eb115746 | ||
|
|
9dc583ffcb | ||
|
|
971024ab99 | ||
|
|
b6da350c20 | ||
|
|
4f90f50eb2 | ||
|
|
84b9a2aefb | ||
|
|
e70416c78c | ||
|
|
4fb1202c30 | ||
|
|
2245731fc8 | ||
|
|
2b87de1000 | ||
|
|
063c4e9720 | ||
|
|
cc2ca58721 | ||
|
|
7b99275ceb | ||
|
|
478766c600 | ||
|
|
0ae080723c | ||
|
|
f3d6a64156 | ||
|
|
da8f7b29c1 | ||
|
|
8900744fc4 | ||
|
|
71578cb2ef | ||
|
|
8d091f6821 | ||
|
|
9c7c22ed20 | ||
|
|
f9a78e9b45 | ||
|
|
13afd3301f | ||
|
|
a27f3f737f | ||
|
|
bc46045cc7 | ||
|
|
47979871de | ||
|
|
c9675b3f7d | ||
|
|
79f73ccca1 | ||
|
|
74c4ca68e5 | ||
|
|
2a186ab8dd | ||
|
|
6b98323806 | ||
|
|
52ac389661 | ||
|
|
393c1d4bee | ||
|
|
9b8e0eb5c5 | ||
|
|
33f2a23bd8 | ||
|
|
d40538968f | ||
|
|
28ee5fbf2e | ||
|
|
26f234fdbd | ||
|
|
d6fd613272 | ||
|
|
480e76d98e | ||
|
|
8b06468490 | ||
|
|
ea3d4ced5e | ||
|
|
bb7ddd11f1 | ||
|
|
05e7db2362 | ||
|
|
34e0a939e6 | ||
|
|
0da8ed2daa | ||
|
|
0caa0371dc | ||
|
|
7a266113ed | ||
|
|
e725780c6d | ||
|
|
0fb6b4eaf6 | ||
|
|
403220c69a | ||
|
|
6150106dee | ||
|
|
e073075834 | ||
|
|
fca9843fc2 | ||
|
|
169caaf66f | ||
|
|
dea3e326ba | ||
|
|
26ecd7fd1b | ||
|
|
f877f87b01 | ||
|
|
d33851367b | ||
|
|
4c5d753ade | ||
|
|
bc6f69ad54 | ||
|
|
ee2ea11458 | ||
|
|
e1b2956cdb | ||
|
|
8757800888 | ||
|
|
d99f258d61 | ||
|
|
b8bf4319ac | ||
|
|
7ae642e6fa | ||
|
|
70204b4464 | ||
|
|
e0895be7e9 | ||
|
|
815220a3c6 | ||
|
|
668574e4f0 | ||
|
|
adb9a6bc15 | ||
|
|
597d5aff02 | ||
|
|
896b21e78b | ||
|
|
816b84be02 | ||
|
|
290a9b7804 | ||
|
|
ac7b6c6a3d | ||
|
|
3ccb209bfd | ||
|
|
f2ce84c161 | ||
|
|
52a6856f6c | ||
|
|
a73c555574 | ||
|
|
25838ba9b0 | ||
|
|
e6b9e82b2f | ||
|
|
de8c3ba968 | ||
|
|
de56faf862 | ||
|
|
89233d2b87 | ||
|
|
75a00928b5 | ||
|
|
a3834eef46 | ||
|
|
b38d472d7c | ||
|
|
96bfe7ccb7 | ||
|
|
146f98d026 | ||
|
|
ec578ba231 | ||
|
|
29d94f71f3 | ||
|
|
a878dd3837 | ||
|
|
93d3eb662f | ||
|
|
73c08c1ecd | ||
|
|
ea05f81024 | ||
|
|
e16014e448 | ||
|
|
15b1609d88 | ||
|
|
51751a7a05 | ||
|
|
0ae60b8b61 | ||
|
|
7af9fa36d7 | ||
|
|
3886641887 | ||
|
|
b651d3f622 | ||
|
|
d72825e279 | ||
|
|
6e73fbedb0 | ||
|
|
e9f99e5a3f | ||
|
|
9f44de50eb | ||
|
|
ec1faf02ed | ||
|
|
2397abaee5 | ||
|
|
b11bf8eb79 | ||
|
|
f3460ec840 | ||
|
|
fbe14b79cc | ||
|
|
ebd45878c3 | ||
|
|
91881fad6b | ||
|
|
c2cd4934e7 | ||
|
|
48485c27ec | ||
|
|
6e80d89d13 | ||
|
|
8a2a845a16 | ||
|
|
376dcf50ec | ||
|
|
751f3abd95 | ||
|
|
6e98adecdd | ||
|
|
64ae1ec367 | ||
|
|
b937b18e3d | ||
|
|
fc8ca7d82c | ||
|
|
7efeed54de | ||
|
|
52c0df24e3 | ||
|
|
9d0ebe3427 | ||
|
|
1a90c0d031 | ||
|
|
c5e35cc330 | ||
|
|
da32bb5f51 | ||
|
|
7b7440d44a | ||
|
|
e90c6a33bd | ||
|
|
8cf8f6abad | ||
|
|
097e74979f | ||
|
|
e5920c57aa | ||
|
|
c714b724da | ||
|
|
f0697d8f27 | ||
|
|
27309d6714 | ||
|
|
c0631468db | ||
|
|
d10647a592 | ||
|
|
269ac78a98 | ||
|
|
5c665c562a | ||
|
|
4d784b8fc7 | ||
|
|
dd027f09ac | ||
|
|
2a16353cf6 | ||
|
|
8c2e40e291 | ||
|
|
5f1ca4113d | ||
|
|
846f0350d8 | ||
|
|
95a235e239 | ||
|
|
b7bf2fad84 | ||
|
|
d0537f2ee4 | ||
|
|
495afae178 | ||
|
|
83f279410c | ||
|
|
34f51ae0b2 | ||
|
|
5b619c7832 | ||
|
|
376191b31f | ||
|
|
2c507c891c | ||
|
|
73f6c29559 | ||
|
|
6cac0347ae | ||
|
|
f5de1ea5c8 | ||
|
|
161e336d97 | ||
|
|
d2a4cebe54 | ||
|
|
88255d3d3d | ||
|
|
1cd2266da1 | ||
|
|
288c3ee9c2 | ||
|
|
cae34096c7 | ||
|
|
27c73cf9e9 | ||
|
|
84ea6afd01 | ||
|
|
e342b1f7bd | ||
|
|
9f32021d07 | ||
|
|
8ba66c9833 | ||
|
|
2012045798 | ||
|
|
7ec4d16403 | ||
|
|
8cc6853c34 | ||
|
|
3600f2f91b | ||
|
|
7ed05e7d2d | ||
|
|
8aac6a6b18 | ||
|
|
5fc07842eb | ||
|
|
aa2c56e117 | ||
|
|
97c48ed6d2 | ||
|
|
4e45e69247 | ||
|
|
0e881f2999 | ||
|
|
f4c6bdf204 | ||
|
|
099a5fbad9 | ||
|
|
e60c711fdc | ||
|
|
0a41fe2541 | ||
|
|
2ee463fa33 | ||
|
|
398406082a | ||
|
|
277c1101fc | ||
|
|
ee0dd462b8 | ||
|
|
09541c52e1 | ||
|
|
9548f341bf | ||
|
|
b4b473185f | ||
|
|
d46b68230c | ||
|
|
109658731b | ||
|
|
ec07549d5e | ||
|
|
90be3cddf7 | ||
|
|
0a71ca6739 | ||
|
|
2928d39d58 | ||
|
|
407fdab487 | ||
|
|
9e633bddef | ||
|
|
327d067305 | ||
|
|
acb9829159 | ||
|
|
b0535bff34 | ||
|
|
0da90f8092 | ||
|
|
efb8e7c0e4 | ||
|
|
80d6779210 | ||
|
|
32a45cf635 | ||
|
|
45c7bac2d0 | ||
|
|
59ae667cce | ||
|
|
50bf4017d6 | ||
|
|
5f721f01d3 | ||
|
|
f2afd98725 | ||
|
|
2882576479 | ||
|
|
2ff1dcc391 | ||
|
|
dd43594ad6 | ||
|
|
c17ae703c7 | ||
|
|
9da5010265 | ||
|
|
39cd7a29df | ||
|
|
86b37782fe | ||
|
|
1ee8a9f257 | ||
|
|
704223e5d0 | ||
|
|
7ee2418f60 | ||
|
|
14a2f98521 | ||
|
|
7dbc821731 | ||
|
|
0d2a88bafc | ||
|
|
072518ed96 | ||
|
|
863507ea28 | ||
|
|
0b7f73fa8b | ||
|
|
37727448f6 | ||
|
|
74df63a28a | ||
|
|
bb62a01c0d | ||
|
|
7e0e9db4d2 | ||
|
|
48e8e85617 | ||
|
|
8f19f14004 | ||
|
|
aac4ac0fae | ||
|
|
31d12517f0 | ||
|
|
8098229b55 | ||
|
|
8ea9cb1d34 | ||
|
|
6a401bcfbb | ||
|
|
20fd071c4e | ||
|
|
db654d5e59 | ||
|
|
abfff79095 | ||
|
|
8aafd893ed | ||
|
|
21118d5373 | ||
|
|
2c7262b033 | ||
|
|
dc1be35dbb | ||
|
|
ee3f4c21be | ||
|
|
bc002937ad | ||
|
|
2af95d2339 | ||
|
|
d69d5b498d | ||
|
|
cfe04a2aaf | ||
|
|
c6baa34812 | ||
|
|
52177cffcd | ||
|
|
344bcf1448 | ||
|
|
45292700d4 | ||
|
|
c742a1dbe9 | ||
|
|
1cb7d5126c | ||
|
|
e7da4992f3 | ||
|
|
c5e36eb323 | ||
|
|
4dbf4a214d | ||
|
|
ae77c609ff | ||
|
|
195911ce4e | ||
|
|
d2983b977c | ||
|
|
824a86c503 | ||
|
|
a0a8428483 | ||
|
|
87a472c2d7 | ||
|
|
a926c7eafd | ||
|
|
b630ccc8e6 | ||
|
|
0426b37f32 | ||
|
|
e1d9f863c6 | ||
|
|
24cc711a70 | ||
|
|
7c7b392789 | ||
|
|
fab0f4f337 | ||
|
|
aee26c3550 | ||
|
|
1e929e2c6c | ||
|
|
0585bc5aef | ||
|
|
ee4e0c646d | ||
|
|
ea6ff2defe | ||
|
|
ab77ee7f5a | ||
|
|
ce6ace4b8a | ||
|
|
3e4d160626 |
4
.github/helper/install.sh
vendored
4
.github/helper/install.sh
vendored
@@ -4,14 +4,10 @@ set -e
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
|
||||
curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash
|
||||
sudo apt update
|
||||
sudo apt remove mysql-server mysql-client
|
||||
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev
|
||||
|
||||
pip cache remove mysqlclient
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
|
||||
@@ -236,6 +236,10 @@ frappe.treeview_settings["Account"] = {
|
||||
root_company,
|
||||
]);
|
||||
} else {
|
||||
const node = treeview.tree.get_selected_node();
|
||||
if (node.is_root) {
|
||||
frappe.throw(__("Cannot create root account."));
|
||||
}
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
@@ -254,7 +258,8 @@ frappe.treeview_settings["Account"] = {
|
||||
].treeview.page.fields_dict.root_company.get_value() ||
|
||||
frappe.flags.ignore_root_company_validation) &&
|
||||
node.expandable &&
|
||||
!node.hide_add
|
||||
!node.hide_add &&
|
||||
!node.is_root
|
||||
);
|
||||
},
|
||||
click: function () {
|
||||
@@ -270,12 +275,14 @@ frappe.treeview_settings["Account"] = {
|
||||
label: __("View Ledger"),
|
||||
click: function (node, btn) {
|
||||
frappe.route_options = {
|
||||
account: node.label,
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
company:
|
||||
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
|
||||
};
|
||||
if (node.parent_label) {
|
||||
frappe.route_options["account"] = node.label;
|
||||
}
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
btnClass: "hidden-xs",
|
||||
|
||||
@@ -0,0 +1,817 @@
|
||||
{
|
||||
"country_code": "au",
|
||||
"name": "Australia - Chart of Accounts with Account Numbers",
|
||||
"tree": {
|
||||
"Assets": {
|
||||
"Current Assets": {
|
||||
"Cash On Hand": {
|
||||
"Cash On Hand": {
|
||||
"account_number": "11010",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"account_number": "110",
|
||||
"is_group": 1
|
||||
},
|
||||
"Cash at Bank": {
|
||||
"Every Day Bank Account": {
|
||||
"account_number": "11510",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Business Savings Account": {
|
||||
"account_number": "11520"
|
||||
},
|
||||
"Business Term Deposit": {
|
||||
"account_number": "11530"
|
||||
},
|
||||
"account_number": "115",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Receivables": {
|
||||
"Trade Debtors": {
|
||||
"account_number": "12010",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Provision for Doubtful Debts": {
|
||||
"account_number": "12020"
|
||||
},
|
||||
"Sundry Debtors": {
|
||||
"account_number": "12030"
|
||||
},
|
||||
"Debtor Refund": {
|
||||
"account_number": "12040"
|
||||
},
|
||||
"account_number": "120",
|
||||
"is_group": 1
|
||||
},
|
||||
"Inventory": {
|
||||
"Stock On Hand": {
|
||||
"account_number": "13010",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"WIP - Work In Progress - Manufacturing": {
|
||||
"account_number": "13020"
|
||||
},
|
||||
"account_number": "130",
|
||||
"is_group": 1
|
||||
},
|
||||
"Prepayments": {
|
||||
"Prepayments": {
|
||||
"account_number": "14010"
|
||||
},
|
||||
"Provisional Tax Paid": {
|
||||
"account_number": "14020"
|
||||
},
|
||||
"account_number": "140",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "11",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Assets": {
|
||||
"Plant & Equipment": {
|
||||
"Plant & Equipment": {
|
||||
"account_number": "16010",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Plant & Equipment": {
|
||||
"account_number": "16020",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "160",
|
||||
"is_group": 1
|
||||
},
|
||||
"Motor Vehicle": {
|
||||
"Motor Vehicle": {
|
||||
"account_number": "16110",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Motor Vehicle": {
|
||||
"account_number": "16120",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "161",
|
||||
"is_group": 1
|
||||
},
|
||||
"Office Equipment": {
|
||||
"Office Furniture & Equipment": {
|
||||
"account_number": "16210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Office Furniture & Equipment": {
|
||||
"account_number": "16220",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "162",
|
||||
"is_group": 1
|
||||
},
|
||||
"Computer Equipment": {
|
||||
"Computer Equipment": {
|
||||
"account_number": "16310",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Computer Equipment": {
|
||||
"account_number": "16320",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "163",
|
||||
"is_group": 1
|
||||
},
|
||||
"Building": {
|
||||
"Buildings": {
|
||||
"account_number": "16410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Buildings": {
|
||||
"account_number": "16420",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"CWIP - Construction Work In Progress": {
|
||||
"account_number": "16430",
|
||||
"account_type": "Capital Work in Progress"
|
||||
},
|
||||
"Accumulated Depreciation - Others": {
|
||||
"account_number": "16440",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "164",
|
||||
"is_group": 1
|
||||
},
|
||||
"Related Party": {
|
||||
"Loan to Party 1": {
|
||||
"account_number": "17010"
|
||||
},
|
||||
"account_number": "170",
|
||||
"is_group": 1
|
||||
},
|
||||
"Investments & Unlisted Entities": {
|
||||
"Investment - Entity 1": {
|
||||
"account_number": "17510"
|
||||
},
|
||||
"account_number": "175",
|
||||
"is_group": 1
|
||||
},
|
||||
"Intagible Assets": {
|
||||
"Goodwill": {
|
||||
"account_number": "18010"
|
||||
},
|
||||
"Opening Balance Temporary ": {
|
||||
"account_number": "18090",
|
||||
"account_type": "Temporary"
|
||||
},
|
||||
"account_number": "180",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "16",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "1",
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Liabilities": {
|
||||
"Current Liabilities": {
|
||||
"Trade Payables - Current": {
|
||||
"Trade Creditors": {
|
||||
"account_number": "21010",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Goods Received Not Invoiced": {
|
||||
"account_number": "21050",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Service Received Not Invoiced": {
|
||||
"account_number": "21060"
|
||||
},
|
||||
"Asset Received Not Invoiced": {
|
||||
"account_number": "21070",
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
},
|
||||
"account_number": "210",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Payables - Current": {
|
||||
"Accrued Expenses": {
|
||||
"account_number": "21510"
|
||||
},
|
||||
"Payroll - Wages Clearing": {
|
||||
"account_number": "21550"
|
||||
},
|
||||
"Payroll - Superannuation Deductions": {
|
||||
"account_number": "21555"
|
||||
},
|
||||
"Payroll - Misc Deductions": {
|
||||
"account_number": "21560"
|
||||
},
|
||||
"Payroll - Withholding Tax Payable": {
|
||||
"account_number": "21565"
|
||||
},
|
||||
"account_number": "215",
|
||||
"is_group": 1
|
||||
},
|
||||
"GST": {
|
||||
"GST Payments to ATO": {
|
||||
"account_number": "22030"
|
||||
},
|
||||
"Provision for PAYG Tax": {
|
||||
"account_number": "22040"
|
||||
},
|
||||
"account_number": "220",
|
||||
"account_type": "Tax",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Current": {
|
||||
"Credit Card - VISA": {
|
||||
"account_number": "22510"
|
||||
},
|
||||
"account_number": "225",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Overdraft": {
|
||||
"Bank Overdraft Cash at Bank": {
|
||||
"account_number": "23010"
|
||||
},
|
||||
"account_number": "230",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Finance": {
|
||||
"Trade Finance": {
|
||||
"account_number": "23510"
|
||||
},
|
||||
"account_number": "235",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities": {
|
||||
"Finance Lease - Current": {
|
||||
"account_number": "24010"
|
||||
},
|
||||
"account_number": "240",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "24510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "24520"
|
||||
},
|
||||
"account_number": "245",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "21",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Liabilities": {
|
||||
"Trade & Other Payables - Non Current": {
|
||||
"Loan Account - Party 1": {
|
||||
"account_number": "25010"
|
||||
},
|
||||
"account_number": "250",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Non Current": {
|
||||
"Non Current Liability - Director Loan": {
|
||||
"account_number": "25510"
|
||||
},
|
||||
"account_number": "255",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Loans - Non Current": {
|
||||
"Bank Loan 1 - Non Current": {
|
||||
"account_number": "26010"
|
||||
},
|
||||
"account_number": "260",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities - Non Current": {
|
||||
"Finance Lease - Non Current": {
|
||||
"account_number": "27010"
|
||||
},
|
||||
"account_number": "270",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions - Non Current": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "27510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "27520"
|
||||
},
|
||||
"account_number": "275",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "25",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "2",
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Equity": {
|
||||
"Equity": {
|
||||
"Owner's/Shareholder's Equity": {
|
||||
"Owner's/Shareholders Capital": {
|
||||
"account_number": "31010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Owner's/Shareholders Drawings": {
|
||||
"account_number": "31020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "310",
|
||||
"is_group": 1
|
||||
},
|
||||
"Earnings": {
|
||||
"Current Year Earnings": {
|
||||
"account_number": "35010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "35020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "350",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "31",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "3",
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Revenue": {
|
||||
"Revenue": {
|
||||
"Sales Revenue": {
|
||||
"Sales Income": {
|
||||
"account_number": "41010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Freight Income": {
|
||||
"account_number": "41020",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Other Income": {
|
||||
"account_number": "41030",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Service Income": {
|
||||
"account_number": "41040",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"account_number": "410",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Revenue": {
|
||||
"Commission Received": {
|
||||
"account_number": "42010"
|
||||
},
|
||||
"Discounts Received": {
|
||||
"account_number": "42020"
|
||||
},
|
||||
"Interest received": {
|
||||
"account_number": "42030"
|
||||
},
|
||||
"Profit/Loss on Sales of Assets": {
|
||||
"account_number": "42040"
|
||||
},
|
||||
"Rent Received": {
|
||||
"account_number": "42050"
|
||||
},
|
||||
"Sundry Income": {
|
||||
"account_number": "42060"
|
||||
},
|
||||
"account_number": "420",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "41",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "4",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods Sold": {
|
||||
"Cost of Goods Sold": {
|
||||
"account_number": "51010",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Freight Expenses (sales related)": {
|
||||
"account_number": "51020"
|
||||
},
|
||||
"Discounts Given": {
|
||||
"account_number": "51030"
|
||||
},
|
||||
"Subcontracting Charges": {
|
||||
"account_number": "51040"
|
||||
},
|
||||
"account_number": "510",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other COGS": {
|
||||
"Purchases - Miscellaneous": {
|
||||
"account_number": "52010"
|
||||
},
|
||||
"Duty & Customs Fees": {
|
||||
"account_number": "52020",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Freight Inwards": {
|
||||
"account_number": "52030",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "52040",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Wirte Off": {
|
||||
"account_number": "52050",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Valuation Expenses": {
|
||||
"account_number": "52060",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Asset Valuation Expenses": {
|
||||
"account_number": "52070",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"account_number": "520",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "51",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "5",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Expenses": {
|
||||
"Fixed Expenses": {
|
||||
"Payroll & Related Expenses": {
|
||||
"Salaries & Wages": {
|
||||
"account_number": "61010"
|
||||
},
|
||||
"Superannuation": {
|
||||
"account_number": "61015"
|
||||
},
|
||||
"Staff Amenities - GST Paid": {
|
||||
"account_number": "61020"
|
||||
},
|
||||
"Staff Amenities - GST Free": {
|
||||
"account_number": "61025"
|
||||
},
|
||||
"Staff Recruitment": {
|
||||
"account_number": "61030"
|
||||
},
|
||||
"Staff Training": {
|
||||
"account_number": "61035"
|
||||
},
|
||||
"Fringe Benefits Tax": {
|
||||
"account_number": "61040"
|
||||
},
|
||||
"Payroll Tax": {
|
||||
"account_number": "61045"
|
||||
},
|
||||
"Workers Compensation": {
|
||||
"account_number": "61050"
|
||||
},
|
||||
"Long Service Leave": {
|
||||
"account_number": "61060"
|
||||
},
|
||||
"Mileage Reimbursement": {
|
||||
"account_number": "61070"
|
||||
},
|
||||
"Overtime": {
|
||||
"account_number": "61080"
|
||||
},
|
||||
"Worksafe Insurance": {
|
||||
"account_number": "61090"
|
||||
},
|
||||
"account_number": "610",
|
||||
"is_group": 1
|
||||
},
|
||||
"Depreciation Expenses": {
|
||||
"Depreciation - Plant & Equipment": {
|
||||
"account_number": "62010",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Motor Vehicle": {
|
||||
"account_number": "62020",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Office Equipment": {
|
||||
"account_number": "62030",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Computer Equipment": {
|
||||
"account_number": "62040",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Building": {
|
||||
"account_number": "62050",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Others": {
|
||||
"account_number": "62510",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"account_number": "620",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "61",
|
||||
"is_group": 1
|
||||
},
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses - Salaries & Wages": {
|
||||
"account_number": "63010"
|
||||
},
|
||||
"Accrued Expenses - Interest": {
|
||||
"account_number": "63020"
|
||||
},
|
||||
"account_number": "630",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "63",
|
||||
"is_group": 1
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"General and Administrative Expenses": {
|
||||
"Low Value Assets less than $300": {
|
||||
"account_number": "64010"
|
||||
},
|
||||
"Office Supplies": {
|
||||
"account_number": "64020"
|
||||
},
|
||||
"Postage & Courier": {
|
||||
"account_number": "64025"
|
||||
},
|
||||
"Printing & Stationery": {
|
||||
"account_number": "64030"
|
||||
},
|
||||
"Registration Fees / Filing Fees": {
|
||||
"account_number": "64040"
|
||||
},
|
||||
"Travel & Accommodation - Local": {
|
||||
"account_number": "64050"
|
||||
},
|
||||
"Travel & Accommodation - Overseas": {
|
||||
"account_number": "64060"
|
||||
},
|
||||
"Relocation Costs": {
|
||||
"account_number": "64070"
|
||||
},
|
||||
"Hire Charges": {
|
||||
"account_number": "64080"
|
||||
},
|
||||
"Repairs & Maintenance": {
|
||||
"account_number": "64210"
|
||||
},
|
||||
"Cleaning Expenses": {
|
||||
"account_number": "64215"
|
||||
},
|
||||
"Uniforms": {
|
||||
"account_number": "64220"
|
||||
},
|
||||
"Security": {
|
||||
"account_number": "64225"
|
||||
},
|
||||
"Subscriptions & Licences": {
|
||||
"account_number": "64510"
|
||||
},
|
||||
"Software Expenses": {
|
||||
"account_number": "64515"
|
||||
},
|
||||
"Marketing Expenses": {
|
||||
"account_number": "64520"
|
||||
},
|
||||
"Advertising Expenses": {
|
||||
"account_number": "64525"
|
||||
},
|
||||
"Website Hosting & Domain Expenses": {
|
||||
"account_number": "64530"
|
||||
},
|
||||
"Computer Repairs / Supplies": {
|
||||
"account_number": "64540"
|
||||
},
|
||||
"Conferences": {
|
||||
"account_number": "64550"
|
||||
},
|
||||
"Consultancy /Contract Services": {
|
||||
"account_number": "64560"
|
||||
},
|
||||
"Training Services": {
|
||||
"account_number": "64570"
|
||||
},
|
||||
"Workshop Supplies": {
|
||||
"account_number": "64580"
|
||||
},
|
||||
"Consumables": {
|
||||
"account_number": "64585"
|
||||
},
|
||||
"Entertainment Expenses - Deductible": {
|
||||
"account_number": "64810"
|
||||
},
|
||||
"Entertainment Expenses - Non Deductible": {
|
||||
"account_number": "64820"
|
||||
},
|
||||
"Amortisation Of Goodwill": {
|
||||
"account_number": "64910"
|
||||
},
|
||||
"General / Miscellaneous Expenses": {
|
||||
"account_number": "64915",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Donations": {
|
||||
"account_number": "64920"
|
||||
},
|
||||
"Client Gifts": {
|
||||
"account_number": "64930"
|
||||
},
|
||||
"Employee Gifts": {
|
||||
"account_number": "64935"
|
||||
},
|
||||
"account_number": "640",
|
||||
"is_group": 1
|
||||
},
|
||||
"Occupancy Expenses": {
|
||||
"Rental Expenses": {
|
||||
"account_number": "65010"
|
||||
},
|
||||
"Property Insurance": {
|
||||
"account_number": "65020"
|
||||
},
|
||||
"Electricity Expenses": {
|
||||
"account_number": "65030"
|
||||
},
|
||||
"Water Rates": {
|
||||
"account_number": "65040"
|
||||
},
|
||||
"Gas Expenses": {
|
||||
"account_number": "65050"
|
||||
},
|
||||
"Property Taxes": {
|
||||
"account_number": "65060"
|
||||
},
|
||||
"Rates": {
|
||||
"account_number": "65070"
|
||||
},
|
||||
"account_number": "650",
|
||||
"is_group": 1
|
||||
},
|
||||
"Communication & Vehicle Expenses": {
|
||||
"Internet Expenses": {
|
||||
"account_number": "66010"
|
||||
},
|
||||
"Mobile Telephone": {
|
||||
"account_number": "66020"
|
||||
},
|
||||
"Telephone Expenses": {
|
||||
"account_number": "66030"
|
||||
},
|
||||
"Motor Vehicle - Fuel Expenses": {
|
||||
"account_number": "66040"
|
||||
},
|
||||
"Motor Vehicle - Parking & Tolls": {
|
||||
"account_number": "66050"
|
||||
},
|
||||
"Motor Vehicle - Registration & Insurance": {
|
||||
"account_number": "66060"
|
||||
},
|
||||
"Motor Vehicle - Service & Repairs": {
|
||||
"account_number": "66070"
|
||||
},
|
||||
"Taxi": {
|
||||
"account_number": "66080"
|
||||
},
|
||||
"account_number": "660",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "64",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non-Operating Expenses": {
|
||||
"Finance Costs": {
|
||||
"Interest - Bank Loans": {
|
||||
"account_number": "67010"
|
||||
},
|
||||
"Interest - Finance Leases": {
|
||||
"account_number": "67020"
|
||||
},
|
||||
"Interest - Other Loans": {
|
||||
"account_number": "67025"
|
||||
},
|
||||
"Insurance": {
|
||||
"account_number": "67030"
|
||||
},
|
||||
"Bank Charges": {
|
||||
"account_number": "67050"
|
||||
},
|
||||
"Rounding off": {
|
||||
"account_number": "67055",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Audit Fees": {
|
||||
"account_number": "67060"
|
||||
},
|
||||
"Accounting Fees": {
|
||||
"account_number": "67070"
|
||||
},
|
||||
"Legal Fees": {
|
||||
"account_number": "67080"
|
||||
},
|
||||
"Management Fees": {
|
||||
"account_number": "67090"
|
||||
},
|
||||
"account_number": "670",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Costs": {
|
||||
"Doubtful Debts": {
|
||||
"account_number": "67510"
|
||||
},
|
||||
"Fines": {
|
||||
"account_number": "67520"
|
||||
},
|
||||
"Debt Collection": {
|
||||
"account_number": "67530"
|
||||
},
|
||||
"Bad Debts": {
|
||||
"account_number": "67540"
|
||||
},
|
||||
"account_number": "675",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "67",
|
||||
"is_group": 1
|
||||
},
|
||||
"Variable Expenses": {
|
||||
"Variable Expenses": {
|
||||
"Bonus & Commissions Paid": {
|
||||
"account_number": "68010"
|
||||
},
|
||||
"Bonus & Commissions To be Paid": {
|
||||
"account_number": "68020"
|
||||
},
|
||||
"Warranty Claims": {
|
||||
"account_number": "68030"
|
||||
},
|
||||
"account_number": "680",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "68",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "6",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Other Income": {
|
||||
"Other Income": {
|
||||
"Interest Income": {
|
||||
"Interest Income": {
|
||||
"account_number": "71010"
|
||||
},
|
||||
"account_number": "710",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Income": {
|
||||
"Gain on Asset Disposal": {
|
||||
"account_number": "73010"
|
||||
},
|
||||
"account_number": "730",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "71",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "7",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Other Expenses": {
|
||||
"Other Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"account_number": "81010"
|
||||
},
|
||||
"account_number": "810",
|
||||
"is_group": 1
|
||||
},
|
||||
"Foreign Exchange Gain/Loss": {
|
||||
"Exchange Loss/Gain - Realized": {
|
||||
"account_number": "82010"
|
||||
},
|
||||
"account_number": "820",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Expenses": {
|
||||
"Loss on Asset Disposal": {
|
||||
"account_number": "83010"
|
||||
},
|
||||
"account_number": "830",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "81",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "8",
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,20 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
@@ -556,7 +558,7 @@
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
@@ -631,6 +633,17 @@
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -59,7 +59,7 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
@@ -149,8 +149,16 @@ class AccountsSettings(Document):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -89,46 +89,64 @@ class BankClearance(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
clearance_date_updated = False
|
||||
invalid_document = []
|
||||
invalid_cheque_date = []
|
||||
entries_to_update = []
|
||||
|
||||
def validate_entry(d):
|
||||
is_valid = True
|
||||
if not d.payment_document:
|
||||
invalid_document.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
if d.clearance_date and d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
invalid_cheque_date.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
return is_valid
|
||||
|
||||
for d in self.get("payment_entries"):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
if d.clearance_date or self.include_reconciled_entries:
|
||||
if validate_entry(d) and (d.clearance_date or self.include_reconciled_entries):
|
||||
if not d.clearance_date:
|
||||
d.clearance_date = None
|
||||
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
entries_to_update.append(d)
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
if invalid_document or invalid_cheque_date:
|
||||
msg = _("<p>Please correct the following row(s):</p><ul>")
|
||||
if invalid_document:
|
||||
msg += _("<li>Payment document required for row(s): {0}</li>").format(
|
||||
", ".join(invalid_document)
|
||||
)
|
||||
|
||||
clearance_date_updated = True
|
||||
if invalid_cheque_date:
|
||||
msg += _("<li>Clearance date must be after cheque date for row(s): {0}</li>").format(
|
||||
", ".join(invalid_cheque_date)
|
||||
)
|
||||
|
||||
if clearance_date_updated:
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
else:
|
||||
msg += "</ul>"
|
||||
frappe.throw(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
msgprint(_("Clearance Date not mentioned"))
|
||||
return
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
|
||||
|
||||
def get_payment_entries_for_bank_clearance(
|
||||
|
||||
@@ -113,6 +113,10 @@ class TestBudget(ERPNextTestSuite):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -126,7 +130,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -462,4 +462,9 @@ def rename_temporarily_named_docs(doctype):
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
)
|
||||
|
||||
for hook_type in ("on_gle_rename", "on_sle_rename"):
|
||||
for hook in frappe.get_hooks(hook_type):
|
||||
frappe.call(hook, newname=newname, oldname=oldname)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
"paid_loan",
|
||||
"inter_company_journal_entry_reference",
|
||||
"column_break98",
|
||||
"bill_no",
|
||||
@@ -310,13 +309,6 @@
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_loan",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Paid Loan",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
|
||||
"fieldname": "inter_company_journal_entry_reference",
|
||||
@@ -599,7 +591,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-17 15:18:13.322681",
|
||||
"modified": "2025-07-06 15:22:58.465131",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
@@ -71,7 +72,6 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
paid_loan: DF.Data | None
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -151,8 +151,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
self.title = self.get_title()
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
journal_accounts = set([x.account for x in self.accounts])
|
||||
@@ -311,9 +311,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
|
||||
@@ -579,6 +579,18 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pay_to_recd_from(self):
|
||||
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
|
||||
jv.pay_to_recd_from = "_Test Receiver"
|
||||
jv.save()
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
|
||||
|
||||
jv.pay_to_recd_from = "_Test Receiver 2"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -46,7 +46,9 @@ from erpnext.accounts.party import (
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_outstanding_invoices,
|
||||
get_reconciliation_effect_date,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -639,7 +641,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate_mandatory(self):
|
||||
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
|
||||
if not self.get(field):
|
||||
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
|
||||
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
|
||||
|
||||
def validate_reference_documents(self):
|
||||
valid_reference_doctypes = self.get_valid_reference_doctypes()
|
||||
@@ -1099,10 +1101,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
if d.reference_doctype in get_advance_payment_doctypes():
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
|
||||
@@ -1384,10 +1383,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
@@ -1570,23 +1566,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", self.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(
|
||||
invoice.reference_doctype, invoice.reference_name, date_field
|
||||
)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
posting_date = nowdate()
|
||||
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
@@ -1780,9 +1760,7 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_lazy_doc(
|
||||
|
||||
@@ -589,7 +589,7 @@ class PaymentReconciliation(Document):
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
|
||||
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
|
||||
|
||||
def validate_entries(self):
|
||||
if not self.get("invoices"):
|
||||
@@ -826,7 +826,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
|
||||
create_gain_loss_journal(
|
||||
company,
|
||||
today(),
|
||||
inv.difference_posting_date,
|
||||
inv.party_type,
|
||||
inv.party,
|
||||
inv.account,
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes, get_currency_precision
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
|
||||
@@ -464,10 +464,7 @@ class PaymentRequest(Document):
|
||||
return create_stripe_subscription(gateway_controller, data)
|
||||
|
||||
def update_reference_advance_payment_status(self):
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
if self.reference_doctype in get_advance_payment_doctypes():
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
ref_doc.set_advance_payment_status()
|
||||
|
||||
@@ -826,8 +823,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
if not references:
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
|
||||
@@ -813,3 +813,33 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
def test_payment_request_on_unreconcile(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt=pi.doctype,
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": pe.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
unreconcile.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -210,8 +210,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return gl_entry
|
||||
|
||||
def get_gle_for_closing_account(self, dimension_balance, dimensions):
|
||||
balance_in_account_currency = flt(dimension_balance.balance_in_account_currency)
|
||||
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
|
||||
debit = balance_in_company_currency if balance_in_company_currency > 0 else 0
|
||||
credit = abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0
|
||||
|
||||
gl_entry = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
@@ -220,14 +222,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"account_currency": frappe.db.get_value(
|
||||
"Account", self.closing_account_head, "account_currency"
|
||||
),
|
||||
"debit_in_account_currency": balance_in_account_currency
|
||||
if balance_in_account_currency > 0
|
||||
else 0,
|
||||
"debit": balance_in_company_currency if balance_in_company_currency > 0 else 0,
|
||||
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||
if balance_in_account_currency < 0
|
||||
else 0,
|
||||
"credit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||
"debit_in_account_currency": debit,
|
||||
"debit": debit,
|
||||
"credit_in_account_currency": credit,
|
||||
"credit": credit,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
"voucher_type": "Period Closing Voucher",
|
||||
"voucher_no": self.name,
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"label": "User Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "pos_opening_entry.company",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
@@ -103,6 +104,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cashier",
|
||||
"options": "User",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -259,7 +261,7 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-06 12:00:31.955176",
|
||||
"modified": "2025-06-14 02:38:14.962291",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
|
||||
@@ -209,13 +209,16 @@ class POSClosingEntry(StatusUpdater):
|
||||
def on_submit(self):
|
||||
consolidate_pos_invoices(closing_entry=self)
|
||||
frappe.publish_realtime(
|
||||
f"poe_{self.pos_opening_entry}_closed",
|
||||
self,
|
||||
f"poe_{self.pos_opening_entry}",
|
||||
message={"operation": "Closed", "doc": self},
|
||||
docname=f"POS Opening Entry/{self.pos_opening_entry}",
|
||||
)
|
||||
|
||||
self.update_sales_invoices_closing_entry()
|
||||
|
||||
def before_cancel(self):
|
||||
self.check_pce_is_cancellable()
|
||||
|
||||
def on_cancel(self):
|
||||
unconsolidate_pos_invoices(closing_entry=self)
|
||||
|
||||
@@ -237,6 +240,15 @@ class POSClosingEntry(StatusUpdater):
|
||||
"Sales Invoice", d.sales_invoice, "pos_closing_entry", self.name if not cancel else None
|
||||
)
|
||||
|
||||
def check_pce_is_cancellable(self):
|
||||
if frappe.db.exists("POS Opening Entry", {"pos_profile": self.pos_profile, "status": "Open"}):
|
||||
frappe.throw(
|
||||
title=_("Cannot cancel POS Closing Entry"),
|
||||
msg=_(
|
||||
"POS Profile - {0} is currently open. Please close the POS or cancel the existing POS Opening Entry before cancelling this POS Closing Entry."
|
||||
).format(frappe.bold(self.pos_profile)),
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -267,7 +279,7 @@ def get_invoices(start, end, pos_profile, user):
|
||||
|
||||
def get_payments(invoices):
|
||||
if not len(invoices):
|
||||
return
|
||||
return []
|
||||
|
||||
invoices_name = [d.name for d in invoices]
|
||||
|
||||
@@ -301,7 +313,7 @@ def get_payments(invoices):
|
||||
|
||||
def get_taxes(invoices):
|
||||
if not len(invoices):
|
||||
return
|
||||
return []
|
||||
|
||||
invoices_name = [d.name for d in invoices]
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
|
||||
if (doc.docstatus == 1 && !doc.is_return) {
|
||||
this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create"));
|
||||
if (["Partly Paid", "Overdue", "Unpaid"].includes(doc.status)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
this.collect_outstanding_payment.bind(this),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
@@ -210,6 +217,138 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
frm: this.frm,
|
||||
});
|
||||
}
|
||||
|
||||
async collect_outstanding_payment() {
|
||||
const total_amount = flt(this.frm.doc.rounded_total) | flt(this.frm.doc.grand_total);
|
||||
const paid_amount = flt(this.frm.doc.paid_amount);
|
||||
const outstanding_amount = flt(this.frm.doc.outstanding_amount);
|
||||
const me = this;
|
||||
|
||||
const table_fields = [
|
||||
{
|
||||
fieldname: "mode_of_payment",
|
||||
fieldtype: "Link",
|
||||
in_list_view: 1,
|
||||
label: __("Mode of Payment"),
|
||||
options: "Mode of Payment",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "amount",
|
||||
fieldtype: "Currency",
|
||||
in_list_view: 1,
|
||||
label: __("Amount"),
|
||||
options: this.frm.doc.currency,
|
||||
reqd: 1,
|
||||
onchange: function () {
|
||||
dialog.fields_dict.payments.df.data.some((d) => {
|
||||
if (d.idx == this.doc.idx) {
|
||||
d.amount = this.value === null ? 0 : this.value;
|
||||
dialog.fields_dict.payments.grid.refresh();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
let amount = 0;
|
||||
for (let d of dialog.fields_dict.payments.df.data) {
|
||||
amount += d.amount;
|
||||
}
|
||||
|
||||
let change_amount = total_amount - (paid_amount + amount);
|
||||
|
||||
dialog.fields_dict.outstanding_amount.set_value(
|
||||
outstanding_amount - amount < 0 ? 0 : outstanding_amount - amount
|
||||
);
|
||||
dialog.fields_dict.paid_amount.set_value(paid_amount + amount);
|
||||
dialog.fields_dict.change_amount.set_value(change_amount < 0 ? change_amount * -1 : 0);
|
||||
},
|
||||
},
|
||||
];
|
||||
const payment_method_data = await this.fetch_pos_payment_methods();
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Collect Outstanding Amount"),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "payments",
|
||||
fieldtype: "Table",
|
||||
label: __("Payments"),
|
||||
cannot_add_rows: false,
|
||||
in_place_edit: true,
|
||||
reqd: 1,
|
||||
data: payment_method_data,
|
||||
fields: table_fields,
|
||||
},
|
||||
{
|
||||
fieldname: "section_break_1",
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "outstanding_amount",
|
||||
fieldtype: "Currency",
|
||||
label: __("Outstanding Amount"),
|
||||
read_only: 1,
|
||||
default: outstanding_amount,
|
||||
},
|
||||
{
|
||||
fieldname: "column_break_1",
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldname: "paid_amount",
|
||||
fieldtype: "Currency",
|
||||
label: __("Paid Amount"),
|
||||
read_only: 1,
|
||||
default: paid_amount,
|
||||
},
|
||||
{
|
||||
fieldname: "change_amount",
|
||||
fieldtype: "Currency",
|
||||
label: __("Change Amount"),
|
||||
read_only: 1,
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Submit"),
|
||||
primary_action(values) {
|
||||
dialog.hide();
|
||||
me.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: "update_payments",
|
||||
args: {
|
||||
payments: values.payments.filter((d) => d.amount != 0),
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert({
|
||||
message: __("Payments updated."),
|
||||
indicator: "green",
|
||||
});
|
||||
me.frm.reload_doc();
|
||||
} else {
|
||||
frappe.show_alert({
|
||||
message: __("Payments could not be updated."),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
async fetch_pos_payment_methods() {
|
||||
const pos_profile = this.frm.doc.pos_profile;
|
||||
if (!pos_profile) return;
|
||||
const pos_profile_doc = await frappe.db.get_doc("POS Profile", pos_profile);
|
||||
const data = [];
|
||||
pos_profile_doc.payments.forEach((pay) => {
|
||||
const { mode_of_payment } = pay;
|
||||
data.push({ mode_of_payment, amount: 0 });
|
||||
});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }));
|
||||
|
||||
@@ -30,24 +30,6 @@
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"col_break4",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -91,14 +73,6 @@
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_47",
|
||||
"total_taxes_and_charges",
|
||||
"loyalty_points_redemption",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"redeem_loyalty_points",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"section_break_49",
|
||||
"coupon_code",
|
||||
"apply_discount_on",
|
||||
@@ -118,13 +92,7 @@
|
||||
"in_words",
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"payments_tab",
|
||||
"payments_section",
|
||||
"cash_bank_account",
|
||||
"payments",
|
||||
@@ -137,6 +105,10 @@
|
||||
"column_break_90",
|
||||
"change_amount",
|
||||
"account_for_change_amount",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
@@ -144,9 +116,41 @@
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"loyalty_points_redemption",
|
||||
"loyalty_points",
|
||||
"loyalty_amount",
|
||||
"redeem_loyalty_points",
|
||||
"column_break_77",
|
||||
"loyalty_program",
|
||||
"loyalty_redemption_account",
|
||||
"loyalty_redemption_cost_center",
|
||||
"contact_and_address_tab",
|
||||
"address_and_contact",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"territory",
|
||||
"col_break4",
|
||||
"shipping_address_name",
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
"payment_schedule",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"more_info_tab",
|
||||
"customer_po_details",
|
||||
"po_no",
|
||||
"column_break_23",
|
||||
"po_date",
|
||||
"edit_printing_settings",
|
||||
"letter_head",
|
||||
"group_same_items",
|
||||
@@ -398,7 +402,6 @@
|
||||
"label": "Customer's Purchase Order Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
@@ -1050,7 +1053,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:(!doc.is_pos && !doc.is_return)",
|
||||
"fieldname": "payment_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1130,8 +1132,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_88",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Changes"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_pos",
|
||||
@@ -1218,7 +1222,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "terms",
|
||||
"fieldname": "terms_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1330,7 +1333,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
|
||||
"options": "\nDraft\nReturn\nCredit Note Issued\nConsolidated\nSubmitted\nPaid\nPartly Paid\nUnpaid\nPartly Paid and Discounted\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1416,6 +1419,8 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate (%)",
|
||||
@@ -1568,12 +1573,32 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payments_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_and_address_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "terms_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Terms"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-06 15:03:19.957277",
|
||||
"modified": "2025-07-18 16:50:30.516162",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1618,6 +1643,7 @@
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
|
||||
@@ -149,7 +149,9 @@ class POSInvoice(SalesInvoice):
|
||||
"Consolidated",
|
||||
"Submitted",
|
||||
"Paid",
|
||||
"Partly Paid",
|
||||
"Unpaid",
|
||||
"Partly Paid and Discounted",
|
||||
"Unpaid and Discounted",
|
||||
"Overdue and Discounted",
|
||||
"Overdue",
|
||||
@@ -220,6 +222,9 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
validate_coupon_code(self.coupon_code)
|
||||
|
||||
def before_submit(self):
|
||||
self.set_outstanding_amount()
|
||||
|
||||
def on_submit(self):
|
||||
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
||||
if not self.is_return and self.loyalty_program:
|
||||
@@ -373,18 +378,6 @@ class POSInvoice(SalesInvoice):
|
||||
_("Payment related to {0} is not completed").format(pay.mode_of_payment)
|
||||
)
|
||||
|
||||
def validate_pos_opening_entry(self):
|
||||
opening_entries = frappe.get_list(
|
||||
"POS Opening Entry", filters={"pos_profile": self.pos_profile, "status": "Open", "docstatus": 1}
|
||||
)
|
||||
if len(opening_entries) == 0:
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Missing"),
|
||||
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
|
||||
frappe.bold(self.pos_profile)
|
||||
),
|
||||
)
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
@@ -537,6 +530,10 @@ class POSInvoice(SalesInvoice):
|
||||
)
|
||||
)
|
||||
|
||||
def set_outstanding_amount(self):
|
||||
total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
self.outstanding_amount = total - flt(self.paid_amount) if total > flt(self.paid_amount) else 0
|
||||
|
||||
def validate_loyalty_transaction(self):
|
||||
if self.redeem_loyalty_points and (
|
||||
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
|
||||
@@ -558,6 +555,8 @@ class POSInvoice(SalesInvoice):
|
||||
self.status = "Draft"
|
||||
return
|
||||
|
||||
total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if not status:
|
||||
if self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
@@ -573,6 +572,14 @@ class POSInvoice(SalesInvoice):
|
||||
self.status = "Overdue and Discounted"
|
||||
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()):
|
||||
self.status = "Overdue"
|
||||
elif (
|
||||
0 < flt(self.outstanding_amount) < total
|
||||
and self.is_discounted
|
||||
and self.get_discounting_status() == "Disbursed"
|
||||
):
|
||||
self.status = "Partly Paid and Discounted"
|
||||
elif 0 < flt(self.outstanding_amount) < total:
|
||||
self.status = "Partly Paid"
|
||||
elif (
|
||||
flt(self.outstanding_amount) > 0
|
||||
and getdate(self.due_date) >= getdate(nowdate())
|
||||
@@ -793,6 +800,48 @@ class POSInvoice(SalesInvoice):
|
||||
if pr:
|
||||
return frappe.get_doc("Payment Request", pr)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_payments(self, payments):
|
||||
if self.status == "Consolidated":
|
||||
frappe.throw(_("Create Payment Entry for Consolidated POS Invoices."))
|
||||
|
||||
paid_amount = flt(self.paid_amount)
|
||||
total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if paid_amount >= total:
|
||||
frappe.throw(title=_("Invoice Paid"), msg=_("This invoice has already been paid."))
|
||||
|
||||
idx = self.payments[-1].idx if self.payments else -1
|
||||
|
||||
for d in payments:
|
||||
idx += 1
|
||||
payment = create_payments_on_invoice(self, idx, frappe._dict(d))
|
||||
paid_amount += flt(payment.amount)
|
||||
payment.submit()
|
||||
|
||||
paid_amount = flt(flt(paid_amount), self.precision("paid_amount"))
|
||||
base_paid_amount = flt(flt(paid_amount * self.conversion_rate), self.precision("base_paid_amount"))
|
||||
outstanding_amount = (
|
||||
flt(flt(total - paid_amount), self.precision("outstanding_amount")) if total > paid_amount else 0
|
||||
)
|
||||
change_amount = (
|
||||
flt(flt(paid_amount - total), self.precision("change_amount")) if paid_amount > total else 0
|
||||
)
|
||||
|
||||
pi = frappe.qb.DocType("POS Invoice")
|
||||
query = (
|
||||
frappe.qb.update(pi)
|
||||
.set(pi.paid_amount, paid_amount)
|
||||
.set(pi.base_paid_amount, base_paid_amount)
|
||||
.set(pi.outstanding_amount, outstanding_amount)
|
||||
.set(pi.change_amount, change_amount)
|
||||
.where(pi.name == self.name)
|
||||
)
|
||||
query.run()
|
||||
self.reload()
|
||||
|
||||
self.set_status(update=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
@@ -944,3 +993,19 @@ def get_item_group(pos_profile):
|
||||
item_groups.extend(get_descendants_of("Item Group", row.item_group))
|
||||
|
||||
return list(set(item_groups))
|
||||
|
||||
|
||||
def create_payments_on_invoice(doc, idx, payment_details):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
|
||||
payment = frappe.new_doc("Sales Invoice Payment")
|
||||
payment.idx = idx
|
||||
payment.mode_of_payment = payment_details.mode_of_payment
|
||||
payment.amount = payment_details.amount
|
||||
payment.base_amount = payment.amount * doc.conversion_rate
|
||||
payment.parent = doc.name
|
||||
payment.parentfield = "payments"
|
||||
payment.parenttype = doc.doctype
|
||||
payment.account = get_bank_cash_account(payment.mode_of_payment, doc.company).get("account")
|
||||
|
||||
return payment
|
||||
|
||||
@@ -18,11 +18,13 @@ frappe.listview_settings["POS Invoice"] = {
|
||||
Draft: "red",
|
||||
Unpaid: "orange",
|
||||
Paid: "green",
|
||||
"Partly Paid": "yellow",
|
||||
Submitted: "blue",
|
||||
Consolidated: "green",
|
||||
Return: "darkgrey",
|
||||
"Unpaid and Discounted": "orange",
|
||||
"Overdue and Discounted": "red",
|
||||
"Partly Paid and Discounted": "yellow",
|
||||
Overdue: "red",
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
|
||||
|
||||
@@ -401,6 +401,50 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
pos_inv.insert()
|
||||
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
|
||||
|
||||
def test_partly_paid_invoices(self):
|
||||
set_allow_partial_payment(self.pos_profile, 1)
|
||||
|
||||
pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 90},
|
||||
)
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
self.assertEqual(pos_inv.paid_amount, 90)
|
||||
self.assertEqual(pos_inv.status, "Partly Paid")
|
||||
|
||||
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 10}])
|
||||
self.assertEqual(pos_inv.paid_amount, 100)
|
||||
self.assertEqual(pos_inv.status, "Paid")
|
||||
|
||||
set_allow_partial_payment(self.pos_profile, 0)
|
||||
|
||||
def test_multi_payment_for_partly_paid_invoices(self):
|
||||
set_allow_partial_payment(self.pos_profile, 1)
|
||||
|
||||
pos_inv = create_pos_invoice(pos_profile=self.pos_profile.name, rate=100, do_not_save=1)
|
||||
pos_inv.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 90},
|
||||
)
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
self.assertEqual(pos_inv.paid_amount, 90)
|
||||
self.assertEqual(pos_inv.status, "Partly Paid")
|
||||
|
||||
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}])
|
||||
self.assertEqual(pos_inv.paid_amount, 95)
|
||||
self.assertEqual(pos_inv.status, "Partly Paid")
|
||||
|
||||
pos_inv.update_payments(payments=[{"mode_of_payment": "Cash", "amount": 5}])
|
||||
self.assertEqual(pos_inv.paid_amount, 100)
|
||||
self.assertEqual(pos_inv.status, "Paid")
|
||||
|
||||
set_allow_partial_payment(self.pos_profile, 0)
|
||||
|
||||
def test_serialized_item_transaction(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
@@ -1089,8 +1133,7 @@ def create_pos_invoice(**args):
|
||||
return pos_inv
|
||||
|
||||
|
||||
def make_batch_item(item_name):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists(item_name):
|
||||
return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1))
|
||||
def set_allow_partial_payment(pos_profile, value):
|
||||
pos_profile.reload()
|
||||
pos_profile.allow_partial_payment = value
|
||||
pos_profile.save()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"merge_invoices_based_on",
|
||||
@@ -113,12 +114,22 @@
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:15.620564",
|
||||
"modified": "2025-07-02 17:08:04.747202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
@@ -179,8 +190,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,10 @@ class POSInvoiceMergeLog(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
|
||||
POSInvoiceReference,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
consolidated_credit_note: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
customer: DF.Link
|
||||
@@ -339,6 +338,11 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
invoice.pos_profile = ""
|
||||
|
||||
# Unset Commission Section
|
||||
invoice.set("sales_partner", None)
|
||||
invoice.set("commission_rate", 0)
|
||||
invoice.set("total_commission", 0)
|
||||
|
||||
return invoice
|
||||
|
||||
def get_new_sales_invoice(self):
|
||||
@@ -584,6 +588,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.company = closing_entry.get("company") if closing_entry else None
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
merge_log.set("pos_invoices", _invoices)
|
||||
|
||||
@@ -491,3 +491,26 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
|
||||
self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
|
||||
def test_company_in_pos_invoice_merge_log(self):
|
||||
"""
|
||||
Test if the company is fetched from POS Closing Entry
|
||||
"""
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
closing_entry = make_closing_entry_from_opening(opening_entry)
|
||||
closing_entry.insert()
|
||||
closing_entry.submit()
|
||||
|
||||
self.assertTrue(frappe.db.exists("POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}))
|
||||
|
||||
pos_merge_log_company = frappe.db.get_value(
|
||||
"POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}, "company"
|
||||
)
|
||||
self.assertEqual(pos_merge_log_company, closing_entry.company)
|
||||
|
||||
@@ -37,6 +37,8 @@ class POSOpeningEntry(StatusUpdater):
|
||||
|
||||
def validate(self):
|
||||
self.validate_pos_profile_and_cashier()
|
||||
self.check_open_pos_exists()
|
||||
self.check_user_already_assigned()
|
||||
self.validate_payment_method_account()
|
||||
self.set_status()
|
||||
|
||||
@@ -49,6 +51,22 @@ class POSOpeningEntry(StatusUpdater):
|
||||
if not cint(frappe.db.get_value("User", self.user, "enabled")):
|
||||
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
|
||||
|
||||
def check_open_pos_exists(self):
|
||||
if frappe.db.exists("POS Opening Entry", {"pos_profile": self.pos_profile, "status": "Open"}):
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Exists"),
|
||||
msg=_(
|
||||
"{0} is open. Close the POS or cancel the existing POS Opening Entry to create a new POS Opening Entry."
|
||||
).format(frappe.bold(self.pos_profile)),
|
||||
)
|
||||
|
||||
def check_user_already_assigned(self):
|
||||
if frappe.db.exists("POS Opening Entry", {"user": self.user, "status": "Open"}):
|
||||
frappe.throw(
|
||||
title=_("Cannot Assign Cashier"),
|
||||
msg=_("Cashier is currently assigned to another POS."),
|
||||
)
|
||||
|
||||
def validate_payment_method_account(self):
|
||||
invalid_modes = []
|
||||
for d in self.balance_details:
|
||||
@@ -71,5 +89,25 @@ class POSOpeningEntry(StatusUpdater):
|
||||
def on_submit(self):
|
||||
self.set_status(update=True)
|
||||
|
||||
def before_cancel(self):
|
||||
self.check_poe_is_cancellable()
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status(update=True)
|
||||
frappe.publish_realtime(
|
||||
f"poe_{self.name}",
|
||||
message={"operation": "Cancelled"},
|
||||
docname=f"POS Opening Entry/{self.name}",
|
||||
)
|
||||
|
||||
def check_poe_is_cancellable(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import get_invoices
|
||||
|
||||
invoices = get_invoices(
|
||||
self.period_start_date, frappe.utils.get_datetime(), self.pos_profile, self.user
|
||||
)
|
||||
if invoices.get("invoices"):
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Cancellation Error"),
|
||||
msg=_("POS Opening Entry cannot be cancelled as unconsolidated Invoices exists."),
|
||||
)
|
||||
|
||||
@@ -3,14 +3,107 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestPOSOpeningEntry(IntegrationTestCase):
|
||||
pass
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
cls.enterClassContext(cls.change_settings("POS Settings", {"invoice_type": "POS Invoice"}))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
|
||||
def setUp(self):
|
||||
# Make stock available for POS Sales
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
|
||||
self.init_user_and_profile = init_user_and_profile
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_pos_opening_entry(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
self.assertEqual(opening_entry.status, "Open")
|
||||
self.assertNotEqual(opening_entry.docstatus, 0)
|
||||
|
||||
def test_multiple_pos_opening_entries_for_same_pos_profile(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
self.assertEqual(opening_entry.status, "Open")
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
def test_multiple_pos_opening_entry_for_multiple_pos_profiles(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry_1 = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
self.assertEqual(opening_entry_1.status, "Open")
|
||||
self.assertEqual(opening_entry_1.user, test_user.name)
|
||||
|
||||
cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager")
|
||||
frappe.set_user(cashier_user.name)
|
||||
|
||||
pos_profile2 = make_pos_profile(name="_Test POS Profile 2")
|
||||
opening_entry_2 = create_opening_entry(pos_profile2, cashier_user.name)
|
||||
|
||||
self.assertEqual(opening_entry_2.status, "Open")
|
||||
self.assertEqual(opening_entry_2.user, cashier_user.name)
|
||||
|
||||
def test_multiple_pos_opening_entry_for_same_pos_profile_by_multiple_user(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
cashier_user = create_user("test_cashier@example.com", "Accounts Manager", "Sales Manager")
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
self.assertEqual(opening_entry.status, "Open")
|
||||
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
create_opening_entry(pos_profile, cashier_user.name)
|
||||
|
||||
def test_user_assignment_to_multiple_pos_profile(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry_1 = create_opening_entry(pos_profile, test_user.name)
|
||||
self.assertEqual(opening_entry_1.user, test_user.name)
|
||||
|
||||
pos_profile2 = make_pos_profile(name="_Test POS Profile 2")
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
create_opening_entry(pos_profile2, test_user.name)
|
||||
|
||||
def test_cancel_pos_opening_entry_without_invoices(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True)
|
||||
|
||||
opening_entry.cancel()
|
||||
self.assertEqual(opening_entry.status, "Cancelled")
|
||||
self.assertNotEqual(opening_entry.docstatus, 1)
|
||||
|
||||
def test_cancel_pos_opening_entry_with_invoice(self):
|
||||
test_user, pos_profile = self.init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name, get_obj=True)
|
||||
|
||||
pos_inv1 = create_pos_invoice(pos_profile=pos_profile.name, rate=100, do_not_save=1)
|
||||
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, opening_entry.cancel)
|
||||
|
||||
|
||||
def create_opening_entry(pos_profile, user):
|
||||
def create_opening_entry(pos_profile, user, get_obj=False):
|
||||
entry = frappe.new_doc("POS Opening Entry")
|
||||
entry.pos_profile = pos_profile.name
|
||||
entry.user = user
|
||||
@@ -24,4 +117,7 @@ def create_opening_entry(pos_profile, user):
|
||||
entry.set("balance_details", balance_details)
|
||||
entry.submit()
|
||||
|
||||
if get_obj:
|
||||
return entry
|
||||
|
||||
return entry.as_dict()
|
||||
|
||||
@@ -26,13 +26,14 @@
|
||||
"auto_add_item_to_cart",
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"action_on_new_invoice",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
"set_grand_total_to_default_mop",
|
||||
"action_on_new_invoice",
|
||||
"allow_partial_payment",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
@@ -423,6 +424,12 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Action on New Invoice",
|
||||
"options": "Always Ask\nSave Changes and Load New Invoice\nDiscard Changes and Load New Invoice"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_partial_payment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Partial Payment"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -451,7 +458,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-05-23 12:12:32.247652",
|
||||
"modified": "2025-06-24 11:19:19.834905",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -32,6 +32,7 @@ class POSProfile(Document):
|
||||
"Always Ask", "Save Changes and Load New Invoice", "Discard Changes and Load New Invoice"
|
||||
]
|
||||
allow_discount_change: DF.Check
|
||||
allow_partial_payment: DF.Check
|
||||
allow_rate_change: DF.Check
|
||||
applicable_for_users: DF.Table[POSProfileUser]
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
|
||||
@@ -169,7 +169,7 @@ class PricingRule(Document):
|
||||
|
||||
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
||||
if tocheck and not self.get(tocheck):
|
||||
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
|
||||
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
|
||||
|
||||
if self.apply_rule_on_other:
|
||||
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)
|
||||
|
||||
@@ -206,6 +206,56 @@ class TestPricingRule(IntegrationTestCase):
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 10)
|
||||
|
||||
def test_unset_group_condition(self):
|
||||
"""
|
||||
If args are not set for group condition, then pricing rule should not be applied.
|
||||
"""
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{"item_code": "_Test Item"}],
|
||||
"currency": "USD",
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"discount_percentage": 10,
|
||||
"applicable_for": "Territory",
|
||||
"territory": "All Territories",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"company": "_Test Company",
|
||||
"price_list": "_Test Price List",
|
||||
"currency": "_Test Currency",
|
||||
"doctype": "Sales Order",
|
||||
"conversion_rate": 1,
|
||||
"price_list_currency": "_Test Currency",
|
||||
"plc_conversion_rate": 1,
|
||||
"order_type": "Sales",
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
}
|
||||
)
|
||||
|
||||
# without territory in customer
|
||||
customer = frappe.get_doc("Customer", "_Test Customer")
|
||||
territory = customer.territory
|
||||
|
||||
customer.territory = None
|
||||
customer.save()
|
||||
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 0)
|
||||
|
||||
customer.territory = territory
|
||||
customer.save()
|
||||
|
||||
def test_pricing_rule_for_variants(self):
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
@@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
|
||||
elif allow_blank:
|
||||
condition = f"ifnull({table}.{field}, '') = ''"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
original_gle = [
|
||||
["Debtors - _TC", 3000.0, 0, "2023-07-01"],
|
||||
[deferred_account, 0.0, 3000, "2023-07-01"],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
|
||||
process_deferred_accounting = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
|
||||
|
||||
# cancel the process deferred accounting document
|
||||
process_deferred_accounting.cancel()
|
||||
|
||||
# check if gl entries are cancelled
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
change_acc_settings()
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
|
||||
@@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("account", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("project", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -79,6 +88,11 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
}
|
||||
},
|
||||
company: function (frm) {
|
||||
frm.set_value("account", "");
|
||||
frm.set_value("cost_center", "");
|
||||
frm.set_value("project", "");
|
||||
},
|
||||
report: function (frm) {
|
||||
let filters = {
|
||||
company: frm.doc.company,
|
||||
|
||||
@@ -376,7 +376,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
"label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -400,7 +400,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified": "2025-07-08 16:52:12.602384",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
||||
self.validate_company_for_table("Cost Center")
|
||||
self.validate_company_for_table("Project")
|
||||
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
@@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
|
||||
if self.company != frappe.get_cached_value("Account", self.account, "company"):
|
||||
frappe.throw(
|
||||
_("Account {0} doesn't belong to Company {1}").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company_for_table(self, doctype):
|
||||
field = frappe.scrub(doctype)
|
||||
if not self.get(field):
|
||||
return
|
||||
|
||||
fieldname = field + "_name"
|
||||
|
||||
values = set(d.get(fieldname) for d in self.get(field))
|
||||
invalid_values = frappe.db.get_all(
|
||||
doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name"
|
||||
)
|
||||
|
||||
if invalid_values:
|
||||
msg = _("<p>Following {0}s doesn't belong to Company {1} :</p>").format(
|
||||
doctype, frappe.bold(self.company)
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<ul>"
|
||||
+ "".join(_("<li>{}</li>").format(frappe.bold(row)) for row in invalid_values)
|
||||
+ "</ul>"
|
||||
)
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = get_statement_dict(doc)
|
||||
|
||||
@@ -1225,6 +1225,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def get_provisional_accounts(self):
|
||||
self.provisional_accounts = frappe._dict()
|
||||
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
|
||||
if not linked_purchase_receipts:
|
||||
return
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item",
|
||||
filters={"parent": ("in", linked_purchase_receipts)},
|
||||
@@ -1309,8 +1312,37 @@ class PurchaseInvoice(BuyingController):
|
||||
net_amt_precision,
|
||||
)
|
||||
|
||||
# Stock ledger value is not matching with the warehouse amount
|
||||
if (
|
||||
if self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
warehouse_debit_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
elif (
|
||||
self.update_stock
|
||||
and voucher_wise_stock_value.get((item.name, item.warehouse))
|
||||
and warehouse_debit_amount
|
||||
@@ -1338,38 +1370,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = (
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("amount_difference_with_purchase_invoice"))
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cost_of_goods_sold_account,
|
||||
"against": item.expense_account,
|
||||
"debit": stock_adjustment_amt,
|
||||
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
|
||||
"remarks": self.get("remarks") or _("Stock Adjustment"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return warehouse_debit_amount
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
|
||||
@@ -1997,6 +1997,7 @@
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -2231,7 +2232,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-17 19:32:31.809658",
|
||||
"modified": "2025-06-26 14:06:56.773552",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe import _, msgprint, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
@@ -474,6 +475,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
self.validate_standalone_serial_nos_customer()
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
@@ -1100,21 +1102,51 @@ class SalesInvoice(SellingController):
|
||||
if self.invoice_type_in_pos == "POS Invoice" and not self.is_return:
|
||||
frappe.throw(_("Transactions using Sales Invoice in POS are disabled."))
|
||||
|
||||
self.validate_pos_opening_entry()
|
||||
|
||||
def validate_full_payment(self):
|
||||
allow_partial_payment = frappe.db.get_value("POS Profile", self.pos_profile, "allow_partial_payment")
|
||||
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if self.is_return and self.paid_amount != invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
if (
|
||||
self.docstatus == 1
|
||||
and not self.is_return
|
||||
and not allow_partial_payment
|
||||
and self.paid_amount < invoice_total
|
||||
):
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
|
||||
if self.paid_amount < invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
def validate_pos_opening_entry(self):
|
||||
opening_entries = frappe.get_all(
|
||||
"POS Opening Entry",
|
||||
fields=["name", "period_start_date"],
|
||||
filters={"pos_profile": self.pos_profile, "status": "Open"},
|
||||
order_by="period_start_date desc",
|
||||
)
|
||||
if not opening_entries:
|
||||
frappe.throw(
|
||||
title=_("POS Opening Entry Missing"),
|
||||
msg=_("No open POS Opening Entry found for POS Profile {0}.").format(
|
||||
frappe.bold(self.pos_profile)
|
||||
),
|
||||
)
|
||||
if len(opening_entries) > 1:
|
||||
frappe.throw(
|
||||
title=_("Multiple POS Opening Entry"),
|
||||
msg=_(
|
||||
"POS Profile - {0} has multiple open POS Opening Entries. Please close or cancel the existing entries before proceeding."
|
||||
).format(self.pos_profile),
|
||||
)
|
||||
if frappe.utils.get_date_str(opening_entries[0].get("period_start_date")) != frappe.utils.today():
|
||||
frappe.throw(
|
||||
title=_("Outdated POS Opening Entry"),
|
||||
msg=_(
|
||||
"POS Opening Entry - {0} is outdated. Please close the POS and create a new POS Opening Entry."
|
||||
).format(opening_entries[0].get("name")),
|
||||
)
|
||||
|
||||
def validate_warehouse(self):
|
||||
super().validate_warehouse()
|
||||
|
||||
@@ -2483,6 +2483,10 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2537,6 +2541,10 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2544,10 +2552,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
@@ -2587,10 +2591,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Days"
|
||||
acc_settings.save()
|
||||
|
||||
def test_validate_inter_company_transaction_address_links(self):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
@@ -2833,7 +2833,9 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(si.items[0].rate, rate)
|
||||
self.assertEqual(target_doc.items[0].rate, rate)
|
||||
|
||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||
check_gl_entries(
|
||||
self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice"
|
||||
)
|
||||
|
||||
def test_internal_transfer_gl_precision_issues(self):
|
||||
# Make a stock queue of an item with two valuations
|
||||
@@ -3033,6 +3035,28 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
# cases where distributed discount amount is not set
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Item",
|
||||
{"name": ["in", [d.name for d in si.items]]},
|
||||
"distributed_discount_amount",
|
||||
0,
|
||||
)
|
||||
|
||||
si.load_from_db()
|
||||
si.additional_discount_account = additional_discount_account
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
expected_gle = [
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
["Discount Account - _TC", 22.0, 0.0, nowdate()],
|
||||
["Service - _TC", 0.0, 100.0, nowdate()],
|
||||
["TDS Payable - _TC", 0.0, 10.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
@@ -4561,6 +4585,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
|
||||
)
|
||||
gl_entries = q.run(as_dict=True)
|
||||
|
||||
doc.assertGreater(len(gl_entries), 0)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_advance_payment_doctypes,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
@@ -84,9 +85,7 @@ class UnreconcilePayment(Document):
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
)
|
||||
if doc.doctype in frappe.get_hooks("advance_payment_payable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_receivable_doctypes"
|
||||
):
|
||||
if doc.doctype in get_advance_payment_doctypes():
|
||||
doc.set_total_advance_paid()
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
@@ -703,7 +703,18 @@ def make_reverse_gl_entries(
|
||||
query.run()
|
||||
else:
|
||||
if not immutable_ledger_enabled:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
gle_names = [x.get("name") for x in gl_entries]
|
||||
|
||||
# if names are available, cancel only that set of entries
|
||||
if not all(gle_names):
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
else:
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
|
||||
modified=%s, modified_by=%s
|
||||
where name in %s and is_cancelled = 0""",
|
||||
(now(), frappe.session.user, tuple(gle_names)),
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
|
||||
@@ -23,6 +23,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.database.schema import get_definition
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@@ -15,7 +15,12 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_advance_payment_doctypes,
|
||||
get_currency_precision,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
|
||||
# This report gives a summary of all Outstanding Invoices considering the following
|
||||
|
||||
@@ -62,6 +67,9 @@ class ReceivablePayableReport:
|
||||
frappe.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
self.advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_receivable_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_payable_doctypes")
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -85,6 +93,7 @@ class ReceivablePayableReport:
|
||||
self.party_details = {}
|
||||
self.invoices = set()
|
||||
self.skip_total_row = 0
|
||||
self.advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.previous_party = ""
|
||||
@@ -121,6 +130,8 @@ class ReceivablePayableReport:
|
||||
self.fetch_ple_in_buffered_cursor()
|
||||
elif self.ple_fetch_method == "UnBuffered Cursor":
|
||||
self.fetch_ple_in_unbuffered_cursor()
|
||||
elif self.ple_fetch_method == "Raw SQL":
|
||||
self.fetch_ple_in_sql_procedures()
|
||||
|
||||
# Build delivery note map against all sales invoices
|
||||
self.build_delivery_note_map()
|
||||
@@ -128,8 +139,7 @@ class ReceivablePayableReport:
|
||||
self.build_data()
|
||||
|
||||
def fetch_ple_in_buffered_cursor(self):
|
||||
query, param = self.ple_query
|
||||
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
|
||||
self.ple_entries = self.ple_query.run(as_dict=True)
|
||||
|
||||
for ple in self.ple_entries:
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
@@ -142,9 +152,8 @@ class ReceivablePayableReport:
|
||||
|
||||
def fetch_ple_in_unbuffered_cursor(self):
|
||||
self.ple_entries = []
|
||||
query, param = self.ple_query
|
||||
with frappe.db.unbuffered_cursor():
|
||||
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
|
||||
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
|
||||
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
|
||||
self.ple_entries.append(ple)
|
||||
|
||||
@@ -181,7 +190,10 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
if (ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no) or (
|
||||
ple.voucher_type in ("Payment Entry", "Journal Entry")
|
||||
and ple.against_voucher_type in self.advance_payment_doctypes
|
||||
):
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
@@ -309,6 +321,79 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
def fetch_ple_in_sql_procedures(self):
|
||||
self.proc = InitSQLProceduresForAR()
|
||||
|
||||
build_balance = f"""
|
||||
begin not atomic
|
||||
declare done boolean default false;
|
||||
declare rec1 row type of `{self.proc._row_def_table_name}`;
|
||||
declare ple cursor for {self.ple_query.get_sql()};
|
||||
declare continue handler for not found set done = true;
|
||||
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.init_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
|
||||
set done = false;
|
||||
open ple;
|
||||
fetch ple into rec1;
|
||||
while not done do
|
||||
call {self.proc.allocate_procedure_name}(rec1);
|
||||
fetch ple into rec1;
|
||||
end while;
|
||||
close ple;
|
||||
end;
|
||||
"""
|
||||
frappe.db.sql(build_balance)
|
||||
|
||||
balances = frappe.db.sql(
|
||||
f"""select
|
||||
name,
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
party,
|
||||
party_account `account`,
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
|
||||
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
|
||||
sum(paid_in_account_currency) `paid_in_account_currency`,
|
||||
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
|
||||
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
|
||||
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
|
||||
as_dict=True,
|
||||
)
|
||||
for x in balances:
|
||||
if self.filters.get("ignore_accounts"):
|
||||
key = (x.voucher_type, x.voucher_no, x.party)
|
||||
else:
|
||||
key = (x.account, x.voucher_type, x.voucher_no, x.party)
|
||||
|
||||
_d = self.build_voucher_dict(x)
|
||||
for field in [
|
||||
"invoiced",
|
||||
"paid",
|
||||
"credit_note",
|
||||
"outstanding",
|
||||
"invoiced_in_account_currency",
|
||||
"paid_in_account_currency",
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
self.voucher_balance[key] = _d
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
total_row = self.total_row_map.get(party)
|
||||
|
||||
@@ -852,18 +937,15 @@ class ReceivablePayableReport:
|
||||
else:
|
||||
query = query.select(ple.remarks)
|
||||
|
||||
query, param = query.walk()
|
||||
|
||||
match_conditions = build_match_conditions("Payment Ledger Entry")
|
||||
if match_conditions:
|
||||
query += " AND " + match_conditions
|
||||
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
|
||||
query = query.where(Criterion.all(match_conditions))
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`"
|
||||
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||
else:
|
||||
query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`"
|
||||
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||
|
||||
self.ple_query = (query, param)
|
||||
self.ple_query = query
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
@@ -1244,3 +1326,134 @@ def get_customer_group_with_children(customer_groups):
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
"""
|
||||
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
|
||||
"""
|
||||
|
||||
_varchar_type = get_definition("Data")
|
||||
_currency_type = get_definition("Currency")
|
||||
# Temporary Tables
|
||||
_voucher_balance_name = "_ar_voucher_balance"
|
||||
_voucher_balance_definition = f"""
|
||||
create temporary table `{_voucher_balance_name}`(
|
||||
name {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
party {_varchar_type},
|
||||
party_account {_varchar_type},
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
invoiced_in_account_currency {_currency_type},
|
||||
paid_in_account_currency {_currency_type},
|
||||
credit_note_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
_row_def_table_name = "_ar_ple_row"
|
||||
_row_def_table_definition = f"""
|
||||
create temporary table `{_row_def_table_name}`(
|
||||
name {_varchar_type},
|
||||
account {_varchar_type},
|
||||
voucher_type {_varchar_type},
|
||||
voucher_no {_varchar_type},
|
||||
against_voucher_type {_varchar_type},
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
account_currency {_varchar_type},
|
||||
amount {_currency_type},
|
||||
amount_in_account_currency {_currency_type}) engine=memory;
|
||||
"""
|
||||
|
||||
# Function
|
||||
genkey_function_name = "ar_genkey"
|
||||
genkey_function_sql = f"""
|
||||
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
|
||||
begin
|
||||
if allocate then
|
||||
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
|
||||
else
|
||||
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
|
||||
end if;
|
||||
end
|
||||
"""
|
||||
|
||||
# Procedures
|
||||
init_procedure_name = "ar_init_tmp_table"
|
||||
init_procedure_sql = f"""
|
||||
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), 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);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
|
||||
allocate_procedure_name = "ar_allocate_to_tmp_table"
|
||||
allocate_procedure_sql = f"""
|
||||
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
|
||||
begin
|
||||
declare invoiced {_currency_type} default 0;
|
||||
declare invoiced_in_account_currency {_currency_type} default 0;
|
||||
declare paid {_currency_type} default 0;
|
||||
declare paid_in_account_currency {_currency_type} default 0;
|
||||
declare credit_note {_currency_type} default 0;
|
||||
declare credit_note_in_account_currency {_currency_type} default 0;
|
||||
|
||||
|
||||
if ple.amount > 0 then
|
||||
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set invoiced = ple.amount;
|
||||
set invoiced_in_account_currency = ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
|
||||
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
|
||||
if (ple.voucher_no = ple.against_voucher_no) then
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
else
|
||||
set credit_note = -1 * ple.amount;
|
||||
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
else
|
||||
set paid = -1 * ple.amount;
|
||||
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
|
||||
end if;
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), 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;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
existing_procedures = frappe.db.get_routines()
|
||||
|
||||
if self.genkey_function_name not in existing_procedures:
|
||||
frappe.db.sql(self.genkey_function_sql)
|
||||
|
||||
if self.init_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.init_procedure_sql)
|
||||
|
||||
if self.allocate_procedure_name not in existing_procedures:
|
||||
frappe.db.sql(self.allocate_procedure_sql)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
|
||||
frappe.db.sql(self._voucher_balance_definition)
|
||||
|
||||
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
|
||||
frappe.db.sql(self._row_def_table_definition)
|
||||
|
||||
@@ -23,6 +23,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
options: "Posting Date\nDue Date",
|
||||
default: "Due Date",
|
||||
},
|
||||
{
|
||||
fieldname: "calculate_ageing_with",
|
||||
label: __("Calculate Ageing With"),
|
||||
fieldtype: "Select",
|
||||
options: "Report Date\nToday Date",
|
||||
default: "Report Date",
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Ageing Range"),
|
||||
|
||||
@@ -30,3 +30,5 @@ frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -79,6 +79,14 @@ class Deferred_Item:
|
||||
return - estimated amount to post for given period
|
||||
Calculated based on already booked amount and item service period
|
||||
"""
|
||||
if self.filters.book_deferred_entries_based_on == "Months":
|
||||
# if the deferred entries are based on service period, use service start and end date
|
||||
return self.calculate_monthly_amount(start_date, end_date)
|
||||
|
||||
else:
|
||||
return self.calculate_days_amount(start_date, end_date)
|
||||
|
||||
def calculate_monthly_amount(self, start_date, end_date):
|
||||
total_months = (
|
||||
(self.service_end_date.year - self.service_start_date.year) * 12
|
||||
+ (self.service_end_date.month - self.service_start_date.month)
|
||||
@@ -105,6 +113,19 @@ class Deferred_Item:
|
||||
|
||||
return base_amount
|
||||
|
||||
def calculate_days_amount(self, start_date, end_date):
|
||||
base_amount = 0
|
||||
total_days = date_diff(self.service_end_date, self.service_start_date) + 1
|
||||
total_booking_days = date_diff(end_date, start_date) + 1
|
||||
already_booked_amount = self.get_item_total()
|
||||
|
||||
base_amount = flt(self.base_net_amount * total_booking_days / flt(total_days))
|
||||
|
||||
if base_amount + already_booked_amount > self.base_net_amount:
|
||||
base_amount = self.base_net_amount - already_booked_amount
|
||||
|
||||
return base_amount
|
||||
|
||||
def make_dummy_gle(self, name, date, amount):
|
||||
"""
|
||||
return - frappe._dict() of a dummy gle entry
|
||||
@@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
else:
|
||||
self.filters = frappe._dict(filters)
|
||||
|
||||
self.filters.book_deferred_entries_based_on = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "book_deferred_entries_based_on"
|
||||
)
|
||||
|
||||
self.period_list = None
|
||||
self.deferred_invoices = []
|
||||
# holds period wise total for report
|
||||
@@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.left_join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.on(
|
||||
(inv_item.name == gle.voucher_detail_no)
|
||||
& (deferred_account_field == gle.account)
|
||||
& (gle.is_cancelled == 0)
|
||||
)
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
inv.posting_date,
|
||||
|
||||
@@ -86,7 +86,7 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac
|
||||
"finance_book": cstr(filters.get("finance_book")),
|
||||
}
|
||||
|
||||
gl_filters["dimensions"] = set(dimension_list)
|
||||
gl_filters["dimensions"] = tuple(set(dimension_list))
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@@ -109,14 +109,19 @@ def get_period_list(
|
||||
|
||||
|
||||
def get_fiscal_year_data(from_fiscal_year, to_fiscal_year):
|
||||
fiscal_year = frappe.db.sql(
|
||||
"""select min(year_start_date) as year_start_date,
|
||||
max(year_end_date) as year_end_date from `tabFiscal Year` where
|
||||
name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
|
||||
{"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year},
|
||||
as_dict=1,
|
||||
from_year_start_date = frappe.get_cached_value("Fiscal Year", from_fiscal_year, "year_start_date")
|
||||
to_year_end_date = frappe.get_cached_value("Fiscal Year", to_fiscal_year, "year_end_date")
|
||||
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(Min(fy.year_start_date).as_("year_start_date"), Max(fy.year_end_date).as_("year_end_date"))
|
||||
.where(fy.year_start_date >= from_year_start_date)
|
||||
.where(fy.year_end_date <= to_year_end_date)
|
||||
)
|
||||
|
||||
fiscal_year = query.run(as_dict=True)
|
||||
return fiscal_year[0] if fiscal_year else {}
|
||||
|
||||
|
||||
@@ -290,6 +295,8 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
||||
"account_name": (
|
||||
f"{_(d.account_number)} - {_(d.account_name)}" if d.account_number else _(d.account_name)
|
||||
),
|
||||
"acc_name": d.account_name,
|
||||
"acc_number": d.account_number,
|
||||
}
|
||||
)
|
||||
for period in period_list:
|
||||
@@ -645,6 +652,25 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None, ca
|
||||
"width": 300,
|
||||
}
|
||||
]
|
||||
if not cash_flow:
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"fieldname": "acc_name",
|
||||
"label": _("Account Name"),
|
||||
"fieldtype": "Data",
|
||||
"width": 250,
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_number",
|
||||
"label": _("Account Number"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120,
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
)
|
||||
if company:
|
||||
columns.append(
|
||||
{
|
||||
|
||||
@@ -209,7 +209,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_err",
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
label: __("Ignore Exchange Rate Revaluation and Gain / Loss Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -210,7 +210,7 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
)
|
||||
|
||||
if filters.get("presentation_currency"):
|
||||
return convert_to_presentation_currency(gl_entries, currency_map)
|
||||
return convert_to_presentation_currency(gl_entries, currency_map, filters)
|
||||
else:
|
||||
return gl_entries
|
||||
|
||||
|
||||
@@ -3,15 +3,12 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests import IntegrationTestCase, change_settings
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
|
||||
class TestGeneralLedger(IntegrationTestCase):
|
||||
@@ -171,90 +168,6 @@ class TestGeneralLedger(IntegrationTestCase):
|
||||
self.assertEqual(data[3]["debit"], 100)
|
||||
self.assertEqual(data[3]["credit"], 100)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
|
||||
def test_debit_in_exchange_gain_loss_account(self):
|
||||
company = "_Test Company"
|
||||
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
|
||||
if not exchange_gain_loss_account:
|
||||
frappe.db.set_value(
|
||||
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
)
|
||||
|
||||
account_name = "_Test Receivable USD - _TC"
|
||||
customer_name = "_Test Customer USD"
|
||||
|
||||
sales_invoice = create_sales_invoice(
|
||||
company=company,
|
||||
customer=customer_name,
|
||||
currency="USD",
|
||||
debit_to=account_name,
|
||||
conversion_rate=85,
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
payment_entry = create_payment_entry(
|
||||
company=company,
|
||||
party_type="Customer",
|
||||
party=customer_name,
|
||||
payment_type="Receive",
|
||||
paid_from=account_name,
|
||||
paid_from_account_currency="USD",
|
||||
paid_to="Cash - _TC",
|
||||
paid_to_account_currency="INR",
|
||||
paid_amount=10,
|
||||
do_not_submit=True,
|
||||
)
|
||||
payment_entry.base_paid_amount = 800
|
||||
payment_entry.received_amount = 800
|
||||
payment_entry.currency = "USD"
|
||||
payment_entry.source_exchange_rate = 80
|
||||
payment_entry.append(
|
||||
"references",
|
||||
frappe._dict(
|
||||
{
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"reference_name": sales_invoice.name,
|
||||
"total_amount": 10,
|
||||
"outstanding_amount": 10,
|
||||
"exchange_rate": 85,
|
||||
"allocated_amount": 10,
|
||||
"exchange_gain_loss": -50,
|
||||
}
|
||||
),
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
journal_entry = frappe.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
|
||||
)
|
||||
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"include_dimensions": 1,
|
||||
"include_default_book_entries": 1,
|
||||
"account": ["_Test Exchange Gain/Loss - _TC"],
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
entry = data[1]
|
||||
self.assertEqual(entry["debit"], 50)
|
||||
self.assertEqual(entry["voucher_type"], "Journal Entry")
|
||||
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
|
||||
|
||||
payment_entry.cancel()
|
||||
payment_entry.delete()
|
||||
sales_invoice.reload()
|
||||
sales_invoice.cancel()
|
||||
sales_invoice.delete()
|
||||
|
||||
def test_ignore_exchange_rate_journals_filter(self):
|
||||
# create a new account with USD currency
|
||||
account_name = "Test Debtors USD"
|
||||
|
||||
@@ -178,7 +178,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Invoice",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
]
|
||||
@@ -310,8 +310,8 @@ def apply_conditions(query, pi, pii, filters):
|
||||
|
||||
def get_items(filters, additional_table_columns):
|
||||
doctype = "Purchase Invoice"
|
||||
pi = frappe.qb.DocType(doctype)
|
||||
pii = frappe.qb.DocType(f"{doctype} Item")
|
||||
pi = frappe.qb.DocType(doctype).as_("invoice")
|
||||
pii = frappe.qb.DocType(f"{doctype} Item").as_("invoice_item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
@@ -331,6 +331,7 @@ def get_items(filters, additional_table_columns):
|
||||
pi.unrealized_profit_loss_account,
|
||||
pii.item_code,
|
||||
pii.description,
|
||||
pii.item_name,
|
||||
pii.item_group,
|
||||
pii.item_name.as_("pi_item_name"),
|
||||
pii.item_group.as_("pi_item_group"),
|
||||
@@ -374,7 +375,7 @@ def get_items(filters, additional_table_columns):
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, pi, pii, filters)
|
||||
query = apply_order_by_conditions(query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import functions as fn
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
from erpnext.accounts.report.utils import get_values_for_columns
|
||||
from erpnext.controllers.taxes_and_totals import ItemWiseTaxDetail
|
||||
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
|
||||
get_customer_details,
|
||||
@@ -198,7 +199,7 @@ def get_columns(additional_table_columns, filters):
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Invoice",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
]
|
||||
@@ -343,7 +344,7 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
|
||||
for opts in ("company", "customer"):
|
||||
if filters.get(opts):
|
||||
query = query.where(si[opts] == filters[opts])
|
||||
@@ -355,10 +356,7 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
query = query.where(si.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
sales_invoice = frappe.db.get_all(
|
||||
"Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
|
||||
)
|
||||
query = query.where(si.name.isin(sales_invoice))
|
||||
query = query.where(sip.mode_of_payment == filters.get("mode_of_payment"))
|
||||
|
||||
if filters.get("warehouse"):
|
||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||
@@ -397,15 +395,15 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
|
||||
return query
|
||||
|
||||
|
||||
def apply_order_by_conditions(query, si, ii, filters):
|
||||
def apply_order_by_conditions(query, filters):
|
||||
if not filters.get("group_by"):
|
||||
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
|
||||
query += "order by invoice.posting_date desc, invoice_item.item_group desc"
|
||||
elif filters.get("group_by") == "Invoice":
|
||||
query += f" order by {ii.parent} desc"
|
||||
query += "order by invoice_item.parent desc"
|
||||
elif filters.get("group_by") == "Item":
|
||||
query += f" order by {ii.item_code}"
|
||||
query += "order by invoice_item.item_code"
|
||||
elif filters.get("group_by") == "Item Group":
|
||||
query += f" order by {ii.item_group}"
|
||||
query += "order by invoice_item.item_group"
|
||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||
filter_field = frappe.scrub(filters.get("group_by"))
|
||||
query += f" order by {filter_field} desc"
|
||||
@@ -415,14 +413,17 @@ def apply_order_by_conditions(query, si, ii, filters):
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
si = frappe.qb.DocType("Sales Invoice").as_("invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item").as_("invoice_item")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(si)
|
||||
.join(sii)
|
||||
.on(si.name == sii.parent)
|
||||
.left_join(sip)
|
||||
.on(sip.parent == si.name)
|
||||
.left_join(item)
|
||||
.on(sii.item_code == item.name)
|
||||
.select(
|
||||
@@ -434,7 +435,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si.is_internal_customer,
|
||||
si.customer,
|
||||
si.remarks,
|
||||
si.territory,
|
||||
fn.IfNull(si.territory, "Not Specified").as_("territory"),
|
||||
si.company,
|
||||
si.base_net_total,
|
||||
sii.project,
|
||||
@@ -457,11 +458,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.base_net_rate,
|
||||
sii.base_net_amount,
|
||||
si.customer_name,
|
||||
si.customer_group,
|
||||
fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"),
|
||||
sii.so_detail,
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
sii.qty,
|
||||
sip.mode_of_payment,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
@@ -481,7 +483,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
if filters.get("customer_group"):
|
||||
query = query.where(si.customer_group == filters["customer_group"])
|
||||
|
||||
query = apply_conditions(query, si, sii, filters, additional_conditions)
|
||||
query = apply_conditions(query, si, sii, sip, filters, additional_conditions)
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
@@ -491,7 +493,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
|
||||
query = apply_order_by_conditions(query, si, sii, filters)
|
||||
query = apply_order_by_conditions(query, filters)
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
@@ -761,25 +763,13 @@ def add_total_row(
|
||||
def get_display_value(filters, group_by_field, item):
|
||||
if filters.get("group_by") == "Item":
|
||||
if item.get("item_code") != item.get("item_name"):
|
||||
value = (
|
||||
cstr(item.get("item_code"))
|
||||
+ "<br><br>"
|
||||
+ "<span style='font-weight: normal'>"
|
||||
+ cstr(item.get("item_name"))
|
||||
+ "</span>"
|
||||
)
|
||||
value = f"{item.get('item_code')}: {item.get('item_name')}"
|
||||
else:
|
||||
value = item.get("item_code", "")
|
||||
elif filters.get("group_by") in ("Customer", "Supplier"):
|
||||
party = frappe.scrub(filters.get("group_by"))
|
||||
if item.get(party) != item.get(party + "_name"):
|
||||
value = (
|
||||
item.get(party)
|
||||
+ "<br><br>"
|
||||
+ "<span style='font-weight: normal'>"
|
||||
+ item.get(party + "_name")
|
||||
+ "</span>"
|
||||
)
|
||||
value = f"{item.get(party)}: {item.get(party + '_name')}"
|
||||
else:
|
||||
value = item.get(party)
|
||||
else:
|
||||
|
||||
@@ -31,3 +31,5 @@ frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["export_hidden_cols"] = true;
|
||||
|
||||
@@ -142,7 +142,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase):
|
||||
accumulated_values=False,
|
||||
)
|
||||
result = execute(filters)
|
||||
columns = [result[0][2], result[0][3]]
|
||||
columns = [result[0][4], result[0][5]]
|
||||
expected = {
|
||||
"account": income_acc,
|
||||
columns[0].get("fieldname"): 450.0,
|
||||
@@ -163,7 +163,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, IntegrationTestCase):
|
||||
columns[1].get("fieldname"): 570.0,
|
||||
}
|
||||
result = execute(filters)
|
||||
columns = [result[0][2], result[0][3]]
|
||||
columns = [result[0][4], result[0][5]]
|
||||
actual = [x for x in result[1] if x.get("account") == income_acc]
|
||||
self.assertEqual(len(actual), 1)
|
||||
actual = actual[0]
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-05-06 12:28:23",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"idx": 6,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-10-06 06:26:07.881340",
|
||||
"letterhead": null,
|
||||
"modified": "2025-07-17 23:16:19.892044",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Partners Commission",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
|
||||
"query": "SELECT\n sales_partner as \"Sales Partner:Link / Sales Partner:220\",\n sum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n sum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n sum(total_commission) as \"Total Commission:Currency:170\",\n sum(total_commission)*100 / sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n (\n SELECT\n sales_partner,\n base_net_total,\n total_commission,\n amount_eligible_for_commission\n FROM\n `tabSales Invoice` \n WHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\n\n UNION ALL\n\n SELECT\n sales_partner,\n base_net_total,\n total_commission,\n amount_eligible_for_commission\n FROM\n `tabPOS Invoice`\n WHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\n ) AS sub\nGROUP BY\n sales_partner\nORDER BY\n \"Total Commission:Currency:120\"",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Sales Partners Commission",
|
||||
"report_type": "Query Report",
|
||||
@@ -26,5 +27,6 @@
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, IntegrationTestCase):
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
from_date = add_to_date(mid_year, days=1)
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": add_to_date(mid_year, days=1),
|
||||
"from_date": from_date,
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
@@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, IntegrationTestCase):
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = "TDS - 3"
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(
|
||||
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
|
||||
)
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
|
||||
inv_1.apply_tds = 1
|
||||
inv_2.tax_withholding_category = "TDS - 3"
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
|
||||
@@ -111,11 +111,18 @@ frappe.query_reports["Trial Balance"] = {
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_group_accounts",
|
||||
label: __("Show Group Accounts"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
formatter: erpnext.financial_statements.formatter,
|
||||
tree: true,
|
||||
name_field: "account",
|
||||
parent_field: "parent_account",
|
||||
export_hidden_cols: true,
|
||||
initial_depth: 3,
|
||||
};
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def validate_filters(filters):
|
||||
|
||||
def get_data(filters):
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
|
||||
|
||||
from `tabAccount` where company=%s order by lft""",
|
||||
filters.company,
|
||||
@@ -401,6 +401,9 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"currency": company_currency,
|
||||
"is_group_account": d.is_group,
|
||||
"acc_name": d.account_name,
|
||||
"acc_number": d.account_number,
|
||||
"account_name": (
|
||||
f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name
|
||||
),
|
||||
@@ -417,6 +420,10 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
data = hide_group_accounts(data)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
@@ -431,6 +438,20 @@ def get_columns():
|
||||
"options": "Account",
|
||||
"width": 300,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_name",
|
||||
"label": _("Account Name"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 250,
|
||||
},
|
||||
{
|
||||
"fieldname": "acc_number",
|
||||
"label": _("Account Number"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
@@ -496,3 +517,12 @@ def prepare_opening_closing(row):
|
||||
row[valid_col] = 0.0
|
||||
else:
|
||||
row[reverse_col] = 0.0
|
||||
|
||||
|
||||
def hide_group_accounts(data):
|
||||
non_group_accounts_data = []
|
||||
for d in data:
|
||||
if not d.get("is_group_account"):
|
||||
d.update(indent=0)
|
||||
non_group_accounts_data.append(d)
|
||||
return non_group_accounts_data
|
||||
|
||||
@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
|
||||
return rate
|
||||
|
||||
|
||||
def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
|
||||
"""
|
||||
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
|
||||
in `currency_info`.
|
||||
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
company_currency = currency_info["company_currency"]
|
||||
|
||||
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
|
||||
exchange_gain_or_loss = False
|
||||
|
||||
if filters and isinstance(filters.get("account"), list):
|
||||
account_filter = filters.get("account")
|
||||
gain_loss_account = frappe.db.get_value("Company", filters.company, "exchange_gain_loss_account")
|
||||
|
||||
exchange_gain_or_loss = len(account_filter) == 1 and account_filter[0] == gain_loss_account
|
||||
|
||||
for entry in gl_entries:
|
||||
debit = flt(entry["debit"])
|
||||
@@ -110,7 +117,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and (debit_in_account_currency or credit_in_account_currency)
|
||||
and not exchange_gain_or_loss
|
||||
):
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Case, Criterion, Table
|
||||
from frappe.query_builder.functions import Count, Max, Round, Sum
|
||||
@@ -179,7 +180,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
|
||||
if doc:
|
||||
doc.fiscal_year = years[0]
|
||||
else:
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -521,7 +522,8 @@ def reconcile_against_document(
|
||||
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
|
||||
dimensions_dict=dimensions_dict,
|
||||
)
|
||||
|
||||
if referenced_row.get("outstanding_amount"):
|
||||
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
@@ -644,10 +646,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
update_advance_paid = []
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if jv_detail.get("reference_type") in advance_payment_doctypes:
|
||||
|
||||
if jv_detail.get("reference_type") in get_advance_payment_doctypes():
|
||||
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
|
||||
|
||||
rev_dr_or_cr = (
|
||||
@@ -731,33 +731,15 @@ def update_reference_in_payment_entry(
|
||||
update_advance_paid = []
|
||||
|
||||
# Update Reconciliation effect date in reference
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", payment_entry.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if payment_entry.book_advance_payments_in_separate_party_account:
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
|
||||
|
||||
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
if d.voucher_detail_no:
|
||||
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
# Update Advance Paid in SO/PO since they are getting unlinked
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if existing_row.get("reference_doctype") in advance_payment_doctypes:
|
||||
if existing_row.get("reference_doctype") in get_advance_payment_doctypes():
|
||||
update_advance_paid.append((existing_row.reference_doctype, existing_row.reference_name))
|
||||
|
||||
if d.allocated_amount <= existing_row.allocated_amount:
|
||||
@@ -805,6 +787,28 @@ def update_reference_in_payment_entry(
|
||||
return row, update_advance_paid
|
||||
|
||||
|
||||
def get_reconciliation_effect_date(reference, company, posting_date):
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(
|
||||
reference.against_voucher_type, reference.against_voucher, date_field
|
||||
)
|
||||
if getdate(reconcile_on) < getdate(posting_date):
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
return reconcile_on
|
||||
|
||||
|
||||
def cancel_exchange_gain_loss_journal(
|
||||
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
|
||||
) -> None:
|
||||
@@ -1019,58 +1023,79 @@ def remove_ref_doc_link_from_pe(
|
||||
per = qb.DocType("Payment Entry Reference")
|
||||
pay = qb.DocType("Payment Entry")
|
||||
|
||||
linked_pe = (
|
||||
query = (
|
||||
qb.from_(per)
|
||||
.select(per.parent)
|
||||
.where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)))
|
||||
.run(as_list=1)
|
||||
)
|
||||
linked_pe = convert_to_list(linked_pe)
|
||||
# remove reference only from specified payment
|
||||
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
|
||||
|
||||
if linked_pe:
|
||||
update_query = (
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
|
||||
.select("*")
|
||||
.where(
|
||||
(per.reference_doctype == ref_type)
|
||||
& (per.reference_name == ref_no)
|
||||
& (per.docstatus.lt(2))
|
||||
& (per.parenttype == "Payment Entry")
|
||||
)
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
update_query = update_query.where(per.parent == payment_name)
|
||||
# update reference only from specified payment
|
||||
if payment_name:
|
||||
query = query.where(per.parent == payment_name)
|
||||
|
||||
update_query.run()
|
||||
reference_rows = query.run(as_dict=True)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
if not reference_rows:
|
||||
return
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [
|
||||
x
|
||||
for x in pe_doc.references
|
||||
if x.reference_doctype == ref_type and x.reference_name == ref_no
|
||||
]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
linked_pe = set()
|
||||
row_names = set()
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
for row in reference_rows:
|
||||
linked_pe.add(row.parent)
|
||||
row_names.add(row.name)
|
||||
|
||||
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
|
||||
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
|
||||
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
|
||||
pay.modified_by, frappe.session.user
|
||||
).where(pay.name == pe).run()
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
update_payment_requests_as_per_pe_references,
|
||||
)
|
||||
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
# Update payment request amount
|
||||
update_payment_requests_as_per_pe_references(reference_rows, cancel=True)
|
||||
|
||||
# Update allocated amounts and modified fields in one go
|
||||
(
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.name.isin(row_names))
|
||||
.where(per.parenttype == "Payment Entry")
|
||||
.run()
|
||||
)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [x for x in pe_doc.references if x.name in row_names]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
|
||||
(
|
||||
qb.update(pay)
|
||||
.set(pay.total_allocated_amount, pe_doc.total_allocated_amount)
|
||||
.set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount)
|
||||
.set(pay.unallocated_amount, pe_doc.unallocated_amount)
|
||||
.set(pay.modified, now())
|
||||
.set(pay.modified_by, frappe.session.user)
|
||||
.where(pay.name == pe)
|
||||
.run()
|
||||
)
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -2270,6 +2295,19 @@ def get_party_types_from_account_type(account_type):
|
||||
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
||||
|
||||
|
||||
def get_advance_payment_doctypes(payment_type=None):
|
||||
"""
|
||||
Get list of advance payment doctypes based on type.
|
||||
:param type: Optional, can be "receivable" or "payable". If not provided, returns both.
|
||||
"""
|
||||
if payment_type:
|
||||
return frappe.get_hooks(f"advance_payment_{payment_type}_doctypes") or []
|
||||
|
||||
return (frappe.get_hooks("advance_payment_receivable_doctypes") or []) + (
|
||||
frappe.get_hooks("advance_payment_payable_doctypes") or []
|
||||
)
|
||||
|
||||
|
||||
def run_ledger_health_checks():
|
||||
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
if health_monitor_settings.enable_health_monitor:
|
||||
@@ -2344,3 +2382,19 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
|
||||
"frequency": "Cron",
|
||||
}
|
||||
).save()
|
||||
|
||||
|
||||
def build_qb_match_conditions(doctype, user=None) -> list:
|
||||
match_filters = build_match_conditions(doctype, user, False)
|
||||
criterion = []
|
||||
if match_filters:
|
||||
from frappe import qb
|
||||
|
||||
_dt = qb.DocType(doctype)
|
||||
|
||||
for filter in match_filters:
|
||||
for d, names in filter.items():
|
||||
fieldname = d.lower().replace(" ", "_")
|
||||
criterion.append(_dt[fieldname].isin(names))
|
||||
|
||||
return criterion
|
||||
|
||||
@@ -659,7 +659,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(schedules, expected_depreciation_before_repair)
|
||||
|
||||
def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair_for(self):
|
||||
def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
gross_purchase_amount=500,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.ui.form.on("Asset Repair", {
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
frm.fields_dict.cost_center.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -177,4 +179,37 @@ frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
var row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "total_value", row.consumed_quantity * row.valuation_rate);
|
||||
},
|
||||
|
||||
pick_serial_and_batch(frm, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let doc = frm.doc;
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.qty = item.consumed_quantity;
|
||||
item.type_of_transaction = item.consumed_quantity > 0 ? "Outward" : "Inward";
|
||||
|
||||
item.title = item.has_serial_no ? __("Select Serial No") : __("Select Batch No");
|
||||
|
||||
if (item.has_serial_no && item.has_batch_no) {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
frm.doc.posting_date = frappe.datetime.get_today();
|
||||
frm.doc.posting_time = frappe.datetime.now_time();
|
||||
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
valuation_rate: r.avg_rate,
|
||||
consumed_quantity: Math.abs(r.total_qty),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"fieldname": "completion_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Completion Date",
|
||||
"mandatory_depends_on": "eval:doc.repair_status==\"Completed\"",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
@@ -127,6 +128,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -259,7 +261,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-27 18:11:40.548727",
|
||||
"modified": "2025-07-18 15:59:53.981224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
@@ -297,10 +299,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class AssetRepair(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
self.validate_asset()
|
||||
self.validate_dates()
|
||||
self.validate_purchase_invoices()
|
||||
self.update_status()
|
||||
@@ -65,8 +66,16 @@ class AssetRepair(AccountsController):
|
||||
self.calculate_total_repair_cost()
|
||||
self.check_repair_status()
|
||||
|
||||
def validate_asset(self):
|
||||
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
|
||||
frappe.throw(
|
||||
_("Asset {0} is in {1} status and cannot be repaired.").format(
|
||||
get_link_to_form("Asset", self.asset), self.asset_doc.status
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (self.failure_date > self.completion_date):
|
||||
if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)):
|
||||
frappe.throw(
|
||||
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
|
||||
)
|
||||
@@ -156,6 +165,13 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.make_gl_entries()
|
||||
|
||||
def cancel_sabb(self):
|
||||
for row in self.stock_items:
|
||||
if sabb := row.serial_and_batch_bundle:
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", sabb)
|
||||
doc.cancel()
|
||||
|
||||
def on_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
if self.get("capitalize_repair_cost"):
|
||||
@@ -167,6 +183,8 @@ class AssetRepair(AccountsController):
|
||||
reschedule_depreciation(self.asset_doc, depreciation_note)
|
||||
self.add_asset_activity()
|
||||
|
||||
self.cancel_sabb()
|
||||
|
||||
def after_delete(self):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
|
||||
@@ -274,7 +292,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -291,7 +309,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Purchase Invoice",
|
||||
"company": self.company,
|
||||
},
|
||||
@@ -326,7 +344,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
@@ -343,7 +361,7 @@ class AssetRepair(AccountsController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Stock Entry",
|
||||
"against_voucher": stock_entry.name,
|
||||
"company": self.company,
|
||||
|
||||
@@ -4,11 +4,12 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import flt, nowdate, nowtime, today
|
||||
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
get_asset_value_after_depreciation,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset,
|
||||
@@ -34,6 +35,33 @@ class TestAssetRepair(IntegrationTestCase):
|
||||
create_item("_Test Stock Item")
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_asset_status(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({"company": "_Test Company", "asset": asset.name, "asset_name": asset.asset_name})
|
||||
self.assertRaises(frappe.ValidationError, asset_repair.save)
|
||||
|
||||
def test_update_status(self):
|
||||
asset = create_asset(submit=1)
|
||||
initial_status = asset.status
|
||||
@@ -360,6 +388,7 @@ def create_asset_repair(**args):
|
||||
|
||||
if args.submit:
|
||||
asset_repair.repair_status = "Completed"
|
||||
asset_repair.completion_date = add_days(args.failure_date, 1)
|
||||
asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center")
|
||||
|
||||
if args.stock_consumption:
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"consumed_quantity",
|
||||
"total_value",
|
||||
"serial_no",
|
||||
"column_break_xzfr",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle"
|
||||
],
|
||||
"fields": [
|
||||
@@ -61,19 +63,29 @@
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xzfr",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 12:01:47.147333",
|
||||
"modified": "2025-06-27 14:52:56.311166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,11 +831,19 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.amount = flt(obj.amount) - flt(obj.billed_amt)
|
||||
target.base_amount = target.amount * flt(source_parent.conversion_rate)
|
||||
target.qty = (
|
||||
target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty)
|
||||
)
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
item_group = get_item_group_defaults(target.item_code, source_parent.company)
|
||||
|
||||
@@ -1319,6 +1319,25 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertFalse(po.per_billed)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
@IntegrationTestCase.change_settings("Buying Settings", {"maintain_same_rate": 0})
|
||||
def test_purchase_invoice_creation_with_partial_qty(self):
|
||||
po = create_purchase_order(qty=100, rate=10)
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.items[0].qty = 42
|
||||
pi.items[0].rate = 7.5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 58)
|
||||
self.assertEqual(pi.items[0].rate, 10)
|
||||
pi.items[0].qty = 8
|
||||
pi.items[0].rate = 5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 50)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -51,6 +51,9 @@ from erpnext.accounts.utils import (
|
||||
get_fiscal_years,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
get_advance_payment_doctypes as _get_advance_payment_doctypes,
|
||||
)
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.controllers.print_settings import (
|
||||
set_print_templates_for_item_table,
|
||||
@@ -383,10 +386,7 @@ class AccountsController(TransactionBase):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
if self.doctype in advance_payment_doctypes:
|
||||
if self.doctype in self.get_advance_payment_doctypes():
|
||||
qb.from_(adv).delete().where(
|
||||
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
|
||||
).run()
|
||||
@@ -1138,6 +1138,10 @@ class AccountsController(TransactionBase):
|
||||
return True
|
||||
|
||||
def set_taxes_and_charges(self):
|
||||
if self.doctype == "Material Request":
|
||||
# Material Request does not have taxes
|
||||
return
|
||||
|
||||
if self.get("taxes") or self.get("is_pos"):
|
||||
return
|
||||
|
||||
@@ -1943,6 +1947,15 @@ class AccountsController(TransactionBase):
|
||||
and self.get("discount_amount")
|
||||
and self.get("additional_discount_account")
|
||||
):
|
||||
# cases where distributed_discount_amount is not patched
|
||||
if not hasattr(self, "__has_distributed_discount_set"):
|
||||
self.__has_distributed_discount_set = any(
|
||||
i.distributed_discount_amount for i in self.get("items")
|
||||
)
|
||||
|
||||
if not self.__has_distributed_discount_set:
|
||||
return item.amount, item.base_amount
|
||||
|
||||
amount += item.distributed_discount_amount
|
||||
base_amount += flt(
|
||||
item.distributed_discount_amount * self.get("conversion_rate"),
|
||||
@@ -2265,9 +2278,9 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
if not paid_amount:
|
||||
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
|
||||
if self.doctype in self.get_advance_payment_doctypes(payment_type="receivable"):
|
||||
new_status = "Not Requested" if paid_amount is None else "Requested"
|
||||
elif self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
|
||||
elif self.doctype in self.get_advance_payment_doctypes(payment_type="payable"):
|
||||
new_status = "Not Initiated" if paid_amount is None else "Initiated"
|
||||
else:
|
||||
total_amount = self.get("rounded_total") or self.get("grand_total")
|
||||
@@ -2921,10 +2934,8 @@ class AccountsController(TransactionBase):
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
|
||||
def get_advance_payment_doctypes(self) -> list:
|
||||
return frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
def get_advance_payment_doctypes(self, payment_type=None) -> list:
|
||||
return _get_advance_payment_doctypes(payment_type=payment_type)
|
||||
|
||||
def make_advance_payment_ledger_for_journal(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
@@ -3981,6 +3992,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import ValidationError, _, msgprint
|
||||
from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, flt, getdate
|
||||
from frappe.utils import cint, flt, format_date, get_link_to_form, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
@@ -81,19 +81,38 @@ class BuyingController(SubcontractingController):
|
||||
)
|
||||
|
||||
def validate_posting_date_with_po(self):
|
||||
po_list = []
|
||||
for item in self.items:
|
||||
if item.purchase_order and item.purchase_order not in po_list:
|
||||
po_list.append(item.purchase_order)
|
||||
po_list = {x.purchase_order for x in self.items if x.purchase_order}
|
||||
|
||||
if not po_list:
|
||||
return
|
||||
|
||||
invalid_po = []
|
||||
po_dates = frappe._dict(
|
||||
frappe.get_all(
|
||||
"Purchase Order",
|
||||
filters={"name": ["in", po_list]},
|
||||
fields=["name", "transaction_date"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
for po in po_list:
|
||||
po_posting_date = frappe.get_value("Purchase Order", po, "transaction_date")
|
||||
if getdate(po_posting_date) > getdate(self.posting_date):
|
||||
frappe.throw(
|
||||
_("Posting Date {0} cannot be before Purchase Order Posting Date {1}").format(
|
||||
frappe.bold(self.posting_date), frappe.bold(po_posting_date)
|
||||
)
|
||||
)
|
||||
po_date = po_dates[po]
|
||||
if getdate(po_date) > getdate(self.posting_date):
|
||||
invalid_po.append((get_link_to_form("Purchase Order", po), format_date(po_date)))
|
||||
|
||||
if not invalid_po:
|
||||
return
|
||||
|
||||
msg = _("<p>Posting Date {0} cannot be before Purchase Order date for the following:</p><ul>").format(
|
||||
frappe.bold(format_date(self.posting_date))
|
||||
)
|
||||
|
||||
for po, date in invalid_po:
|
||||
msg += f"<li>{po} ({date})</li>"
|
||||
msg += "</ul>"
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
def create_package_for_transfer(self) -> None:
|
||||
"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
|
||||
@@ -329,7 +348,7 @@ class BuyingController(SubcontractingController):
|
||||
tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
|
||||
|
||||
for i, item in enumerate(self.get("items")):
|
||||
if item.item_code and item.qty:
|
||||
if item.item_code and (item.qty or item.get("rejected_qty")):
|
||||
item_tax_amount, actual_tax_amount = 0.0, 0.0
|
||||
if i == (last_item_idx - 1):
|
||||
item_tax_amount = total_valuation_amount
|
||||
@@ -368,7 +387,19 @@ class BuyingController(SubcontractingController):
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
if (
|
||||
not net_rate
|
||||
and item.get("rejected_qty")
|
||||
and frappe.get_single_value(
|
||||
"Buying Settings", "set_valuation_rate_for_rejected_materials"
|
||||
)
|
||||
):
|
||||
net_rate = item.rejected_qty * item.net_rate
|
||||
|
||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||
if not qty_in_stock_uom and item.get("rejected_qty"):
|
||||
qty_in_stock_uom = flt(item.rejected_qty * item.conversion_factor)
|
||||
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||
item.valuation_rate = (
|
||||
@@ -508,13 +539,12 @@ class BuyingController(SubcontractingController):
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1))
|
||||
else:
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
d.sales_incoming_rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
* (d.conversion_factor or 1)
|
||||
)
|
||||
|
||||
def validate_for_subcontracting(self):
|
||||
|
||||
@@ -42,14 +42,14 @@ def employee_query(
|
||||
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
|
||||
)
|
||||
|
||||
search_conditions = " or ".join([f"{field} like %(txt)s" for field in fields])
|
||||
mcond = "" if ignore_permissions else get_match_cond(doctype)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabEmployee`
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or employee_name like %(txt)s)
|
||||
and ({key} like %(txt)s or {search_conditions})
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
@@ -62,6 +62,7 @@ def employee_query(
|
||||
"key": searchfield,
|
||||
"fcond": get_filters_cond(doctype, filters, conditions),
|
||||
"mcond": mcond,
|
||||
"search_conditions": search_conditions,
|
||||
}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Abs
|
||||
@@ -38,6 +38,18 @@ def validate_return_against(doc):
|
||||
|
||||
party_type = "customer" if doc.doctype in ("Sales Invoice", "Delivery Note") else "supplier"
|
||||
|
||||
if ref_doc.get(party_type) != doc.get(party_type):
|
||||
frappe.throw(
|
||||
_("The {0} {1} does not match with the {0} {2} in the {3} {4}").format(
|
||||
doc.meta.get_label(party_type),
|
||||
bold(doc.get(party_type)),
|
||||
bold(ref_doc.get(party_type)),
|
||||
ref_doc.doctype,
|
||||
ref_doc.name,
|
||||
),
|
||||
title=_("Party Mismatch"),
|
||||
)
|
||||
|
||||
if (
|
||||
ref_doc.company == doc.company
|
||||
and ref_doc.get(party_type) == doc.get(party_type)
|
||||
|
||||
@@ -61,6 +61,38 @@ class SellingController(StockController):
|
||||
if self.get(table_field):
|
||||
self.set_serial_and_batch_bundle(table_field)
|
||||
|
||||
def validate_standalone_serial_nos_customer(self):
|
||||
if not self.is_return or self.return_against:
|
||||
return
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Delivery Note"]:
|
||||
bundle_ids = [d.serial_and_batch_bundle for d in self.get("items") if d.serial_and_batch_bundle]
|
||||
if not bundle_ids:
|
||||
return
|
||||
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": ("in", bundle_ids), "serial_no": ("is", "set")},
|
||||
pluck="serial_no",
|
||||
)
|
||||
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
if serial_nos := frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"name": ("in", serial_nos), "customer": ("is", "set")},
|
||||
fields=["name", "customer"],
|
||||
):
|
||||
for sn in serial_nos:
|
||||
if sn.customer and sn.customer != self.customer:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Serial No {0} is already assigned to customer {1}. Can only be returned against the customer {1}"
|
||||
).format(frappe.bold(sn.name), frappe.bold(sn.customer)),
|
||||
title=_("Serial No Already Assigned"),
|
||||
)
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super().set_missing_values(for_validate)
|
||||
|
||||
|
||||
@@ -1581,7 +1581,7 @@ def get_accounting_ledger_preview(doc, filters):
|
||||
|
||||
doc.docstatus = 1
|
||||
|
||||
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
|
||||
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note", "Stock Entry"):
|
||||
doc.update_stock_ledger()
|
||||
|
||||
doc.make_gl_entries()
|
||||
@@ -1622,7 +1622,7 @@ def get_stock_ledger_preview(doc, filters):
|
||||
"stock_value_difference",
|
||||
]
|
||||
|
||||
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note"):
|
||||
if doc.get("update_stock") or doc.doctype in ("Purchase Receipt", "Delivery Note", "Stock Entry"):
|
||||
doc.docstatus = 1
|
||||
doc.update_stock_ledger()
|
||||
columns = get_sl_columns(filters)
|
||||
@@ -1757,8 +1757,9 @@ def make_quality_inspections(doctype, docname, items, inspection_type):
|
||||
"sample_size": flt(item.get("sample_size")),
|
||||
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
|
||||
"batch_no": item.get("batch_no"),
|
||||
"child_row_reference": item.get("child_row_reference"),
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
quality_inspection.save()
|
||||
inspections.append(quality_inspection.name)
|
||||
|
||||
@@ -1771,14 +1772,9 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
if allow_force_reposting and frappe.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
@@ -470,7 +470,7 @@ class SubcontractingController(StockController):
|
||||
i += 1
|
||||
|
||||
def __remove_serial_and_batch_bundle(self, item):
|
||||
if item.serial_and_batch_bundle:
|
||||
if item.get("serial_and_batch_bundle"):
|
||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
@@ -607,12 +607,15 @@ class SubcontractingController(StockController):
|
||||
rm_obj.use_serial_batch_fields = 1
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
if self.doctype == "Subcontracting Receipt":
|
||||
if not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
elif self.backflush_based_on == "BOM":
|
||||
self.update_rate_for_supplied_items()
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
@@ -956,7 +959,7 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows)
|
||||
sco_doc.update_reserved_qty_for_subcontracting()
|
||||
sco_doc.update_reserved_qty_for_subcontracting(sco_item_rows)
|
||||
|
||||
def make_sl_entries_for_supplier_warehouse(self, sl_entries):
|
||||
if hasattr(self, "supplied_items"):
|
||||
@@ -1049,7 +1052,7 @@ class SubcontractingController(StockController):
|
||||
|
||||
return supplied_items_cost
|
||||
|
||||
def set_subcontracting_order_status(self):
|
||||
def set_subcontracting_order_status(self, update_bin=True):
|
||||
if self.doctype == "Subcontracting Order":
|
||||
self.update_status()
|
||||
elif self.doctype == "Subcontracting Receipt":
|
||||
@@ -1058,7 +1061,7 @@ class SubcontractingController(StockController):
|
||||
if self.subcontract_orders:
|
||||
for sco in set(self.subcontract_orders):
|
||||
sco_doc = frappe.get_doc("Subcontracting Order", sco)
|
||||
sco_doc.update_status()
|
||||
sco_doc.update_status(update_bin=update_bin)
|
||||
|
||||
def calculate_additional_costs(self):
|
||||
self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
|
||||
|
||||
@@ -47,7 +47,7 @@ def get_columns(filters, trans):
|
||||
def validate_filters(filters):
|
||||
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
|
||||
if not filters.get(f.lower().replace(" ", "_")):
|
||||
frappe.throw(_("{0} is mandatory").format(f))
|
||||
frappe.throw(_("{0} is mandatory").format(_(f)))
|
||||
|
||||
if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")):
|
||||
frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year")))
|
||||
|
||||
@@ -486,7 +486,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -542,11 +542,12 @@
|
||||
"label": "Analytics"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-user",
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2025-01-31 13:40:08.094759",
|
||||
"modified": "2025-06-26 11:02:01.158901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -605,6 +606,7 @@
|
||||
"role": "Sales User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "lead_name,lead_owner,status",
|
||||
"sender_field": "email_id",
|
||||
"sender_name_field": "lead_name",
|
||||
|
||||
@@ -600,7 +600,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -614,21 +614,21 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_source",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Source",
|
||||
"oldfieldname": "source",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "UTM Source"
|
||||
"options": "UTM Source",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_campaign",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Campaign",
|
||||
"oldfieldname": "campaign",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "UTM Campaign"
|
||||
"options": "UTM Campaign",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_analytics",
|
||||
@@ -641,22 +641,23 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Medium",
|
||||
"options": "UTM Medium"
|
||||
"options": "UTM Medium",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_content",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Data",
|
||||
"label": "Content"
|
||||
"label": "Content",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2024-08-20 04:12:29.095761",
|
||||
"modified": "2025-06-26 11:16:13.665866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
@@ -688,6 +689,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status,transaction_date,party_name,opportunity_type,territory,company",
|
||||
"sender_field": "contact_email",
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_columns():
|
||||
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
|
||||
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
|
||||
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State/Province"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Country"),
|
||||
"fieldname": "country",
|
||||
|
||||
@@ -24,7 +24,6 @@ develop_version = "15.x.x-develop"
|
||||
|
||||
app_include_js = "erpnext.bundle.js"
|
||||
app_include_css = "erpnext.bundle.css"
|
||||
web_include_js = "erpnext-web.bundle.js"
|
||||
web_include_css = "erpnext-web.bundle.css"
|
||||
email_css = "email_erpnext.bundle.css"
|
||||
|
||||
@@ -281,6 +280,7 @@ standard_portal_menu_items = [
|
||||
sounds = [
|
||||
{"name": "incoming-call", "src": "/assets/erpnext/sounds/incoming-call.mp3", "volume": 0.2},
|
||||
{"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2},
|
||||
{"name": "numpad-touch", "src": "/assets/erpnext/sounds/numpad-touch.mp3", "volume": 0.8},
|
||||
]
|
||||
|
||||
has_upload_permission = {"Employee": "erpnext.setup.doctype.employee.employee.has_upload_permission"}
|
||||
|
||||
3569
erpnext/locale/ar.po
3569
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
3584
erpnext/locale/bs.po
3584
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
3567
erpnext/locale/cs.po
3567
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
3907
erpnext/locale/de.po
3907
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
3573
erpnext/locale/eo.po
3573
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
3571
erpnext/locale/es.po
3571
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
4094
erpnext/locale/fa.po
4094
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
3569
erpnext/locale/fr.po
3569
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
20735
erpnext/locale/hr.po
20735
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
3567
erpnext/locale/hu.po
3567
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
95902
erpnext/locale/id.po
95902
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
3567
erpnext/locale/it.po
3567
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3571
erpnext/locale/nl.po
3571
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
3569
erpnext/locale/pl.po
3569
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user